반환 값에 따른 정렬 함수의 해석

 

반환 값 < 0 : a가 b보다 앞에 있어야 한다.

반환 값 = 0 : a와 b의 순서를 바꾸지 않는다.

반환 값 > 0 : b가 a보다 앞에 있어야 한다.

 

왜 오름차순이면 return a-b , 내림차순이면 return b-a를 해주는지 확실하게 알 수 있다. 자동으로 위의 규칙을 만족하기 때문이다.

 

예를들어 a,b 두 값이 있고, 내림차순을 원할 때 b-a를 하는 이유는, 만약 a<b 라면  b-a가 양수이므로  b가 앞으로, a가 뒤로 간다.

또 만약 a>b라면 b-a가 음수이므로 a가 앞으로, b가 뒤로 간다. 따라서 내림차순을 자동으로 만족한다. 

 

아래는 정렬 정리.

 

1. 1차원 배열 정렬 (int[])

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] arr = {5, 2, 9, 1, 5, 6};

        Arrays.sort(arr); // 오름차순 정렬
        System.out.println(Arrays.toString(arr)); // [1, 2, 5, 5, 6, 9]
    }
}

 

 

2. 1차원 배열 내림차순 정렬 (Integer[])

import java.util.Arrays;
import java.util.Collections;

public class Main {
    public static void main(String[] args) {
        Integer[] arr = {5, 2, 9, 1, 5, 6};

        Arrays.sort(arr, Collections.reverseOrder()); // 내림차순 정렬
        System.out.println(Arrays.toString(arr)); // [9, 6, 5, 5, 2, 1]
    }
}

주의: int[]는 안 되고 Integer[] 써야 Collections.reverseOrder()가 먹힘.

 

 

3. 리스트 (List<Integer>) 정렬

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(5, 2, 9, 1, 5, 6);

        Collections.sort(list); // 오름차순
        System.out.println(list); // [1, 2, 5, 5, 6, 9]

        list.sort(Collections.reverseOrder()); // 내림차순
        System.out.println(list); // [9, 6, 5, 5, 2, 1]
    }
}

리스트는 Collections.sort() 또는 list.sort() 둘 다 가능.

 

 

4. 2차원 배열 정렬 (특정 인덱스 기준)

(예) 2차원 배열을 [0] 번째 값 기준 오름차순

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[][] arr = {
            {5, 2},
            {1, 7},
            {3, 4}
        };

        // 오름차순
        Arrays.sort(arr, (a, b) -> Integer.compare(a[0], b[0]));
        
        // 내림차순 (b[0], a[0] 순서 바꾸면 내림차순!)
        Arrays.sort(arr, (a, b) -> Integer.compare(b[0], a[0]));
        
        // 만약 0번째 요소가 같으면 1번째 요소 기준 오름차순
        Arrays.sort(arr, (a, b) -> {
        	if (a[0] == b[0]) return Integer.compare(a[1], b[1]);
        	return Integer.compare(a[0], b[0]);
        });
    }
}

 

 

5. 클래스 객체 정렬

(예: Person 클래스의 나이(age) 기준으로 정렬)

import java.util.*;

class Person {
    String name;
    int age;
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class Main {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
            new Person("Alice", 30),
            new Person("Bob", 20),
            new Person("Charlie", 25)
        );

        people.sort(Comparator.comparingInt(p -> p.age));

        for (Person p : people) {
            System.out.println(p.name + " " + p.age);
        }
    }
}

 

  • Comparator.comparingInt(람다)
  • 또는 Comparable을 상속하여 compareTo 오버라이드 해도 됨.
  • 또는 people.sort((p1, p2) -> Integer.compare(p1.age, p2.age)); 이런 식도 가능

 

정리 

상황코드 한줄 요약
int[] 오름차순 Arrays.sort(arr)
Integer[] 내림차순 Arrays.sort(arr, Collections.reverseOrder())
List 오름/내림차순 Collections.sort(list) / list.sort(Collections.reverseOrder())
2차원배열 특정요소 기준 오름차순 Arrays.sort(arr, (a, b) -> Integer.compare(a[idx], b[idx]))
객체 정렬 list.sort(Comparator.comparing(람다))

 

 

 

문제 세트 (정렬 실전 연습)

 

1. int[] 오름차순, 내림차순

[문제1]
정수 배열 arr = {7, 2, 9, 4, 1, 5}가 주어졌을 때,

  • 오름차순 정렬한 결과를 출력하라.
  • 내림차순 정렬한 결과를 출력하라.
import java.util.Arrays;
import java.util.Collections;

public class Test {
    public static void main(String[] args) {

        int[] arr = {7, 2, 9, 4, 1, 5};

        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr)); // [1, 2, 4, 5, 7, 9]

        Integer[] boxedArr = Arrays.stream(arr).boxed().toArray(Integer[]::new);
        Arrays.sort(boxedArr, Collections.reverseOrder());
        System.out.println(Arrays.toString(boxedArr)); // [9, 7, 5, 4, 2, 1]

    }
}

 


 

2. 2차원 int 배열 특정 열 기준 정렬

[문제2]
2차원 배열 arr = { {3, 5}, {1, 7}, {4, 2}, {2, 2} }가 있다.

  • [1] 번째 값을 오름차순으로 정렬하고 출력하라.
import java.util.Arrays;
import java.util.Comparator;

public class Test {
    public static void main(String[] args) {

        int[][] arr = { {3, 5}, {1, 7}, {4, 2}, {2, 2} };

        Arrays.sort(arr, (a,b) -> a[1] - b[1]);
        System.out.println(Arrays.deepToString(arr)); // [[4, 2], [2, 2], [3, 5], [1, 7]]


        // 결과는 위와 같음 (Comparator 직접사용)
        Arrays.sort(arr, Comparator.comparingInt(a -> a[1]));
        System.out.println(Arrays.deepToString(arr)); // [[4, 2], [2, 2], [3, 5], [1, 7]]
    }
}

 

 

[문제3]
위 배열을

  • [1] 번째 값을 오름차순 정렬하고,
  • 만약 [1] 값이 같으면 [0] 값을 내림차순으로 정렬하라.
import java.util.Arrays;
import java.util.Comparator;

public class Test {
    public static void main(String[] args) {

        int[][] arr = { {3, 5}, {1, 7}, {4, 2}, {2, 2} };

        Arrays.sort(arr, (a,b) -> {
            if (a[1] == b[1]) return b[0] - a[0];
            return a[1] - b[1];
        });
        System.out.println(Arrays.deepToString(arr)); // [[4, 2], [2, 2], [3, 5], [1, 7]]


        // Comparator 방식 (가독성용)
        Arrays.sort(arr, Comparator
                .comparingInt((int[] a) -> a[1])           // [1] 오름차순
                .thenComparing((a, b) -> b[0] - a[0]) // [0] 내림차순
        );
        System.out.println(Arrays.deepToString(arr)); // [[4, 2], [2, 2], [3, 5], [1, 7]]

    }
}

 


3. Integer[] 배열 내림차순

[문제4]
정수 배열 arr = {3, 6, 1, 8, 2}가 주어졌을 때,

  • Integer[]로 변환해서 내림차순 정렬하고 출력하라.
import java.util.Arrays;
import java.util.Collections;

public class Test {
    public static void main(String[] args) {

        int[] arr = {3, 6, 1, 8, 2};

        Integer[] boxedArr = Arrays.stream(arr).boxed().toArray(Integer[]::new);

        Arrays.sort(boxedArr, Collections.reverseOrder());

        System.out.println(Arrays.toString(boxedArr)); //[8, 6, 3, 2, 1]

    }
}

4. List<Integer> 오름차순/내림차순

[문제5]
리스트 list = [5, 1, 9, 3, 2]가 주어졌을 때,

  • 오름차순 정렬한 결과 출력
  • 내림차순 정렬한 결과 출력
public class Test {
    public static void main(String[] args) {

        List<Integer> list = Arrays.asList(5,1,9,3,2);
        Collections.sort(list);
        System.out.println(list); // [1, 2, 3, 5, 9]

        Collections.sort(list, Collections.reverseOrder());
        System.out.println(list); // [9, 5, 3, 2, 1]

    }
}

5. 문자열 2차원 배열 정렬

[문제6]
2차원 문자열 배열 arr = { {"banana", "yellow"}, {"apple", "red"}, {"cherry", "red"} }가 있다.

  • [0]번째 문자열을 기준으로 오름차순 정렬하라.
import java.util.Arrays;

public class Test {
    public static void main(String[] args) {

        String[][] arr = { {"banana", "yellow"}, {"apple", "red"}, {"cherry", "red"} };

        Arrays.sort(arr,(a,b) -> a[0].compareTo(b[0]));

        System.out.println(Arrays.deepToString(arr));
    }
}

6. 문자열 다중 조건 정렬

[문제7]
2차원 문자열 배열 arr = { {"banana", "yellow"}, {"banana", "green"}, {"apple", "red"}, {"cherry", "red"} }가 있다.

  • [0]번째 문자열을 오름차순 정렬하고,
  • [0]번째가 같으면 [1]번째 문자열을 오름차순으로 정렬하라.
import java.util.Arrays;
import java.util.Comparator;

public class Test {
    public static void main(String[] args) {

        String[][] arr = { {"apple", "yellow"}, {"banana", "red"}, {"apple", "red"} };

        Arrays.sort(arr,(a,b) -> {
            if (a[0].equals(b[0])) {
                return a[1].compareTo(b[1]);
            }
            return a[0].compareTo(b[0]);
        });

        System.out.println(Arrays.deepToString(arr)); // [[apple, red], [apple, yellow], [banana, red]]


        // comparator 사용
        Arrays.sort(arr, Comparator.comparing((String[] a) -> a[0]) // 0기준 오름차순
                .thenComparing(a -> a[1])); // 1기준 오름차순
                // 오름차순 원할시엔 thenComparing((a,b) -> b[1].compareTo(a[1]);
        System.out.println(Arrays.deepToString(arr));
    }
}

 

 

의존성 주입 (Dependency Injection)

 

소프트웨어 개발에서 코드의 유지보수성과 확장성을 높이기 위해 "의존성 주입(Dependency Injection, DI)"이라는 개념이 자주 사용된다. 특히 Java에서는 DI를 활용하면 객체 간 결합도를 낮추고 코드의 유연성을 극대화할 수 있다. DI를 활용하면 객체 간의 직접적인 의존성을 제거하고, 대신 외부에서 객체를 주입받도록 설계할 수 있다.

이 포스팅에서는 Java에서 의존성 주입을 어떻게 구현하는지, 그리고 DI를 활용했을 때 얻을 수 있는 장점에 대해 알아보겠다.


1. 의존성 문제와 기존 방식의 한계

기존 방식의 문제점

아래 예제는 EmailService 클래스를 활용하는 애플리케이션을 구현한 코드다.

public class EmailService {
    public void sendEmail(String message, String receiver) {
        System.out.println("Email sent to " + receiver + " with Message=" + message);
    }
}

 

이 EmailService 클래스를 사용하는 MyApplication 클래스를 구현해보자.

public class MyApplication {
    private EmailService email = new EmailService();
    
    public void processMessages(String msg, String rec) {
        this.email.sendEmail(msg, rec);
    }
}

 

그리고 클라이언트 코드도 작성해보자.

public class MyLegacyTest {
    public static void main(String[] args) {
        MyApplication app = new MyApplication();
        app.processMessages("Hi Pankaj", "pankaj@abc.com");
    }
}

이 코드에서 MyApplication 클래스는 EmailService 인스턴스를 직접 생성하고 있다. 이는 다음과 같은 문제를 초래한다.

  1. 하드코딩된 의존성 - EmailService에 강하게 결합되어 있어, 다른 메시지 서비스로 변경하려면 코드를 수정해야 한다.
  2. 유연성 부족 - SMS나 Facebook 메시지 같은 기능을 추가하려면 새로운 클래스를 만들어야 한다.
  3. 테스트 어려움 - EmailService가 직접 생성되므로, 단위 테스트 시 Mock 객체를 활용하기 어렵다.

이 문제를 해결하기 위해 의존성 주입을 적용해보자.


2. 의존성 주입 적용하기

서비스 인터페이스 정의

먼저, 메시지 전송 기능을 추상화하는 인터페이스를 만들자.

public interface MessageService {
    void sendMessage(String msg, String rec);
}

구체적인 서비스 구현

이제 MessageService를 구현하는 EmailServiceImpl과 SMSServiceImpl 클래스를 작성하자.

public class EmailServiceImpl implements MessageService {
    @Override
    public void sendMessage(String msg, String rec) {
        System.out.println("Email sent to " + rec + " with Message=" + msg);
    }
}
public class SMSServiceImpl implements MessageService {
    @Override
    public void sendMessage(String msg, String rec) {
        System.out.println("SMS sent to " + rec + " with Message=" + msg);
    }
}

서비스 소비자 (Consumer)

이제 서비스 인터페이스를 이용하는 MyDIApplication 클래스를 구현하자.

public interface Consumer {
    void processMessages(String msg, String rec);
}
public class MyDIApplication implements Consumer {
    private MessageService service;
    
    public MyDIApplication(MessageService svc) {
        this.service = svc;
    }
    
    @Override
    public void processMessages(String msg, String rec) {
        this.service.sendMessage(msg, rec);
    }
}

이제 MyDIApplication 클래스는 MessageService 인터페이스에 의존하므로, 특정 구현체(EmailServiceImpl, SMSServiceImpl)에 직접 의존하지 않는다.

인젝터 인터페이스

public interface MessageServiceInjector {
    Consumer getConsumer();
}

인젝터 클래스

이제 각 서비스별 인젝터 클래스를 작성하자.

public class EmailServiceInjector implements MessageServiceInjector {
    @Override
    public Consumer getConsumer() {
        return new MyDIApplication(new EmailServiceImpl());
    }
}
public class SMSServiceInjector implements MessageServiceInjector {
    @Override
    public Consumer getConsumer() {
        return new MyDIApplication(new SMSServiceImpl());
    }
}

이제 애플리케이션에서 이메일과 SMS를 전송하는 기능을 실행할 수 있다.

public class MyMessageDITest {
    public static void main(String[] args) {
        String msg = "Hi Pankaj";
        String email = "pankaj@abc.com";
        String phone = "4088888888";
        
        MessageServiceInjector injector = new EmailServiceInjector();
        Consumer app = injector.getConsumer();
        app.processMessages(msg, email);
        
        injector = new SMSServiceInjector();
        app = injector.getConsumer();
        app.processMessages(msg, phone);
    }
}

이제 MyDIApplication은 MessageService 인터페이스를 통해 서비스를 이용하며, 실제 서비스 객체는 인젝터에서 주입받는다. 덕분에 새로운 메시지 서비스를 추가할 때 MessageService를 구현한 클래스와 해당하는 인젝터 클래스만 추가하면 된다.

 

단위 테스트 코드

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;

public class MyDIApplicationJUnitTest {
    private MessageServiceInjector injector;

    @Before
    public void setUp() {
        injector = new MessageServiceInjector() {
            @Override
            public Consumer getConsumer() {
                return new MyDIApplication(new MessageService() {
                    @Override
                    public void sendMessage(String msg, String rec) {
                        System.out.println("Mock Message Service implementation");
                    }
                });
            }
        };
    }

    @Test
    public void test() {
        Consumer consumer = injector.getConsumer();
        consumer.processMessages("Hi Pankaj", "pankaj@abc.com");
    }

    @After
    public void tearDown() {
        injector = null;
    }
}

이제 JUnit 테스트에서 mock을 활용하여 DI가 적용된 애플리케이션을 쉽게 검증할 수 있다.


 

3. 의존성 주입의 장점과 단점

장점

  • 관심사의 분리: 서비스 객체를 직접 생성하지 않으므로, 애플리케이션 코드가 보다 깔끔해진다.
  • 유연성 증가: 새로운 메시지 서비스를 쉽게 추가할 수 있다.
  • 테스트 용이성: Mock 객체를 사용하여 단위 테스트가 가능해진다.

단점

  • 구현 복잡성 증가: 단순한 애플리케이션이라면 굳이 DI를 사용할 필요가 없다.
  • 런타임 오류 가능성: 컴파일 타임에 발견할 수 있는 오류가 런타임까지 지연될 수 있다.

4. 결론

의존성 주입은 애플리케이션의 확장성과 유지보수성을 높이는 강력한 디자인 패턴이다. Java에서 DI를 적용하면 객체 간 결합도를 낮추고, 인터페이스 기반으로 설계할 수 있어 테스트가 용이해진다. Spring Framework와 같은 DI 컨테이너를 사용하면 DI를 더욱 편리하게 적용할 수도 있다.

다만, 모든 프로젝트에 DI가 반드시 필요한 것은 아니다. 필요에 따라 적절한 디자인 패턴을 선택하는 것이 중요하다. 하지만, 복잡한 애플리케이션을 개발할 때 DI를 도입하면 코드의 유연성과 확장성이 크게 향상될 것이다.

코딩을 할 때, 특히 알고리즘 문제를 해결할 때 자료형을 변환해야 할 때가 많습니다.

파이썬 같은 동적 타입의 인터프리터 언어면 비교적 쉽게 변환하겠지만 정적 컴파일러 언어인 자바의 특성상 자료형 변환에서 은근 시간을 잡아먹기도 합니다.

 

프로젝트를 진행하면서 @AuthenticationPrincipal로 받은 userPrincipal에서 getMember()를 통해 현재 로그인된 사용자 정보를 가져온다.

 

문제 발생은 userPrincipal에서 가져온 Member에서 연관관계에 있는 객체 조회( 예를 들어 게시글 )를 하기 위해 lazy 로딩을 하려고 할 때 발생했다. proxy ~ 'no Session'  LazyInitializationException을 내뱉으며 조회가 안됐다.

 

그 이유를 찾아보니 아래와 같았다.

 

FetchType이 Lazy일땐 먼저 프록시(빈 객체) 객체를 불러온다.

필요할 때 이 객체를 조회하면 DB에서 객체를 조회하는 쿼리가 발생하면서, 프록시 객체가 DB에 저장된 객체로 변한다.

이러한 lazy 로딩은 영속화 되어 있는 상태에서만 가능하다. 컨트롤러, 서비스, 레포지토리단까지 OSIV가 기본으로 실행되고 있으므로 OSIV가 적용되는 범위 안에서는 Lazy 로딩이 오류가 나지 않는다. 하지만 filter는 OSIV 범위 밖이기 때문에 영속화가 되지 않은 객체를 가져온다.

 

기본적으로 Spring Security에서 인증을 하기 위해서는 먼저 필터를 거치는데, 이는 컨트롤러보다 앞단에 존재하고, OSIV의 적용범위가 아니다. 따라서 컨트롤러에서 @AuthenticationPrincipal 등을 이용해서 가져오는 사용자 정보는 필터단에서 가져오는 영속화 되지 않은 상태이므로, lazy로 설정되어 있는 연관객체를 조회하면 LazyInitializationException이 발생하는 것이다. 

 

 

 

문제 해결 방법을 하나씩 살펴보자.


1. 불러올 객체의 패치타입을 FetchType.EAGER로 변경


이 방법은 매우 간단하지만, 하지만 실무에선 N+1 문제로, 대부분 LAZY를 사용한다고 한다. 실무에선 복잡한 쿼리가 많아서 직접 쿼리를 짜는 경우가 많은데 JPQL 로 모든 MEMBER를 불러올때 TEAM이 EAGER로 되어있으면 멤버 한명을 불러올때마다 팀을 조회하는 쿼리가 나간다. 따라서 MEMBER가 N명이면, 쿼리는 1개 날렸는데 결과적으로 쿼리가 N개가 더 추가돼서 총 N+1 나가는게 N+1문제다. 이렇듯 즉시로딩은 불필요한 쿼리가 발생할 수 있기 때문에 사용을 지양한다.N+1 문제는 JPQL fetch join이나, 엔티티 그래프 기능으로 해결할 수 있다고 한다.
나중에 추가하겠다.

@OneToMany: LAZY
@ManyToOne: EAGER
@ManyToMany: LAZY
@OneToOne: EAGER

엔티티를 조회시 엔티티 조회 쿼리 + 연관관계가 EAGER로 설정된 각각의 엔티티들을 조회하는 쿼리까지 나간다. 연관관계가 LAZY로 설정된 엔티티들은 조회 X.


2. @AuthenticationPrincipal로 불러온 Member 객체에서 id를 get 해서 find 메소드로 repository에서 다시 member를 찾는다(영속성 컨텍스트 활성화) 그 후 getposts() 한다 => 비효율적으로 느껴진다. 멤버에서 다시 멤버를 찾는다..?

3. 영속성 컨텍스트가 존재할때 미리 필요한 entity(post)를 활성화 시킨다.
이방법은 위에서 설명한 eager랑 비슷한 결인데, 스프링 시큐리티에서 적용하는 방법도 있다.

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final MemberRepository userRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Member user = userRepository.findByEmail(email);
        if (user == null) {
            throw new UsernameNotFoundException("Can not find email.");
        }
        user.getPosts().size(); // 여기서 미리 활성화 시켜줌
        return new UserPrincipal(user);
    }
}


이 방법은 항상 멤버가 조회될때 같이 post가 조회된다는 단점이 있다. 따라서 탈락이라고 생각한다. 그리고 필터단은 OSIV 범위가 아니기 때문에 findByEmail 후 끝나기버리기 때문에 @Transactional 어노테이션을 같이 붙여야 한다.


4. OSIV를 필터단까지 적용시킨다.
OSIV default값은 true다. 따라서 컨트롤러까지 영속성 컨텍스트가 적용이 된다. 하지만 
@AuthenticationPrincipal에서 얻어지는 UserDetails는 Spring Security filter단에서 적용된다. 필터는 컨트롤러보다 더 앞단에 있으므로 OSIV가 필터에도 적용되도록 변경하는 것이다. 이러면 @AuthenticationPrincipal를 사용하였을 때 UserDetails도 영속된 상태로 불러진다.

@Component
@Configuration
public class OpenEntityManagerConfig {
    @Bean
    public FilterRegistrationBean<OpenEntityManagerInViewFilter> openEntityManagerInViewFilter() {
        FilterRegistrationBean<OpenEntityManagerInViewFilter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new OpenEntityManagerInViewFilter());
        filterFilterRegistrationBean.setOrder(Integer.MIN_VALUE); // 예시를 위해 최우선 순위로 Filter 등록
        return filterFilterRegistrationBean;
    }
}



5.  jpa repository에서 

List<Post> findByMember(Member member);
List<Post> findByMember_MemberId(Long memberId);


로 찾는 방법이다. 
findBy(외래키객체) 형식 또는
findBy(외래키객체)_(외래키객체)(외래키필드) 형식으로 찾을 수 있다.

 

 

1번 탈락
2번은 멤버를 조회하는 쿼리 + 포스트 조회 쿼리 총 두개가 발생한다.

3번 탈락
4번의 단점은 잘 모르겠는데 필터단까지 영속성 컨텍스트가 적용되므로 어떠한 낭비가 생기지 않을까?
5번은 포스트 조회 쿼리만 발생한다.

 

일단 나는 4번으로 선택하였다.

 

 

 

 

 

 


주제를 벗어나서, @ManyToOne에서 지연로딩의 쿼리는 어떻게 되는걸까?
예를들어 Post 클래스에서 Member는 N:1의 관계다. 그럼 Default Fetch Type은 EAGER다.
따라서 Post를 조회할 시 Post 테이블에 Member가 left outer join 되면서 Member까지 조회한다. 만약 LAZY로 바꾼다면 단순히 Post만 조회된다. 따라서 Member까지 조회할 필요가 없을 시 Fetch Type을 LAZY로 바꾸는게 좋다. (웬만하면 다 LAZY로 하자)

public class Post {

	...
    
    ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "memberId")
    private Member member;
    }



참고 사이트
http://jaynewho.com/post/39
https://zzang9ha.tistory.com/347
https://tecoble.techcourse.co.kr/post/2020-08-31-entity-lifecycle-1/
https://tecoble.techcourse.co.kr/post/2020-09-20-entity-lifecycle-2/

코딩을 할때 내림차순 정렬, 객체 비교 등을 위해 정렬을 직접 구현할 때가 많다.

오늘은 Comparable 인터페이스의 compareTo 메소드와 Comparator 인터페이스의 compare 메소드의 차이를 정리해보려 한다.

 

먼저, sort 와 관련된 리턴값 규칙이 있다. 외우자.

 

두 값 a,b를 비교할 때

 

음수 : a가 b보다 앞에 있어야 한다.

0 : 두 원소가 같다.

양수 : b가 a보다 앞에 있어야 한다.

 

위 규칙을 숙지 한 상태에서, 아래 코드를 보자.

 

기본적으로 정렬을 구현할 때 Comparator 인터페이스의 메소드를 재정의 할 수 있다.

Comparable의 compareTo

        @Override
        public int compareTo(Stage o) {
            if (failure < o.failure) {return -1;}
            else if (failure > o.failure) {return 1;}
            else return 0;
        }

현재 객체에서 파라미터 객체 o와 비교를 한다.

Stage 클래스는 예시를 위해서 임의로 만든 클래스이다.  Stage 객체에는 failure 라는 필드가 있다.

만약 failure 값을 기준으로 두 객체의 크기를 비교판단 하고 싶다면 위와 같이 메서드를 정의하면 된다.

 

간단하다. 메소드 재정의를 통해 원하는 필드값으로 현재 객체와 타 객체(파라미터)를 비교하고 리턴값을 통해 비교판단이 가능하다. 위의 경우엔 기존 객체를 a, 파라미터로 들어온 객체를 b라 생각하면 된다.

파라미터 객체가 더 클땐 -1을 반환하고, 현재 객체가 더 클땐 양수를 반환하므로, 오름차순이다.

 

이제 Comparator의 compare를 보자. (내림차순)

            @Override
            public int compare(Stage o1, Stage o2) {
                if (o1.failure < o2.failure) {
                    return 1;
                } else if (o1.failure > o2.failure) {
                    return -1;
                } else return 0;
            }

compare는 하나의 파라미터가 아닌 두개의 파라미터를 받아서 서로 비교를 한다.

 

위에서 말했듯 리턴값에 따른 규칙은 정해져 있다. 외우자.

음수 : o1이 o2보다 앞에 있어야 한다.

0 : 두 원소가 같다.

양수 : o2가 o1보다 앞에 있어야 한다.

 

위 메소드에서는 내림차순 정렬을 위해 리턴값의 부호를 반대로 주었다.정렬은 기본적으로 오름차순이다. 하지만 이렇게 메소드를 재정의 한 Comparator로 정렬(sort)을 하면, 오름차순이 아닌 내림차순으로 정렬을 할 수 있다. o1이 o2보다 작은데 양수를 리턴 하므로 o2가 앞으로 가게 되고, o1이 o2보다 클땐 음수를 줬으므로 o1이 앞에 있게 된다.(그대로)

 

 

compareTo, compare 의 메소드를 구현할 때 조심해야 할 점이 있다.

바로 NaN 값의 처리이다.

NaN 값은 어떤 값과 비교하든 비교판단에서 무조건 false를 내뱉는다.

따라서 if, else if문을 지나 무조건 else문에서 처리된다.

만약 NaN을 가지고 있는 리스트를 정렬할 때, NaN을 원래 인덱스에 그대로 두고 싶다면 else의 리턴값을 0으로 두면 된다.

하지만 아래 코드와 같이

            @Override
            public int compare(Stage o1, Stage o2) {
                if (o1.failure < o2.failure) {
                    return 1;
                } else if (o1.failure == o2.failure) {
                    return 0;
                } else return -1;
            }

else문의 리턴값이 -1이라면 NaN 값은 항상 작다고 판단되어 리스트의 맨 앞에 위치하게 될것이다.

+ Recent posts