[NEXTSTEP] 세 번째, 네 번째 과제를 마무리 하면서 + 후기
대외활동

[NEXTSTEP] 세 번째, 네 번째 과제를 마무리 하면서 + 후기

들어가며

  • 이번 글에서는 세 번째 미션인 로또 과제, 네 번째 미션인 수강신청을 구현하면서 배웠던 점에 대해 정리하고 후기를 작성하고자 합니다.

Cyclic Dependency가 발생하지 않도록 하자

1. Cylic Dependency란?

  • Cyclic Dependency는 두 개 이상의 모듈 또는 클래스가 서로에게 의존할 때 발생합니다.
  • 의존성은 여러 수준에서 나타날 수 있으며 서로 다른 2개의 클래스가 서로의 import문을 추가했다면 Cyclic Dependency가 발생했다고 볼 수 있습니다.
  • 이러한 Cyclic Dependcy는 모놀리식에서 MSA로 분리할 때 애플리케이션 복잡도 증가로 인해 분리가 안 되는 어려움, 순환 참조 문제 등 많은 문제들을 야기합니다.

2. Cyclic Dependency가 발생한 예시 코드

  • 아래 예시 코드는 Cyclic Dependency가 발생한 예시입니다.
public class ClassA {
    private ClassB classB;

    public ClassA(ClassB classB) {
        this.classB = classB;
    }

    public void methodA() {
        System.out.println("Method A");
        classB.methodB();
    }
}

public class ClassB {
    private ClassA classA;

    public ClassB(ClassA classA) {
        this.classA = classA;
    }

    public void methodB() {
        System.out.println("Method B");
        classA.methodA();
    }
}

  • ClassA는 ClassB에 의존하고, ClassB는 다시 ClassA에 의존합니다. 이런 경우 Cyclic Dependency가 발생했다고 정의합니다.

3. Cyclic Dependency를 해결하는 방법

  • Cyclic Dependency를 해결하는 일반적인 방법은 의존성을 역전(DIP)시키거나, 중간 계층을 도입하거나, 의존성 주입을 사용하는 것입니다.

3.1 의존성 역전 원칙 (Dependency Inversion Principle)

  • 의존성을 인터페이스나 추상 클래스를 통해 역전시켜 Cyclic Dependency를 제거할 수 있습니다.
public interface IClassA {
    void methodA();
}

public interface IClassB {
    void methodB();
}

public class ClassA implements IClassA {
    private IClassB classB;

    public ClassA(IClassB classB) {
        this.classB = classB;
    }

    public void methodA() {
        System.out.println("Method A");
        classB.methodB();
    }
}

public class ClassB implements IClassB {
    private IClassA classA;

    public ClassB(IClassA classA) {
        this.classA = classA;
    }

    public void methodB() {
        System.out.println("Method B");
        classA.methodA();
    }
}

  • 이제 ClassA와 ClassB는 서로 구체 클래스에 의존하지 않고 인터페이스에 의존합니다.

3.2 의존성 주입 (Dependency Injection)

@Component
@RequiredArgsConstructor
public class ClassA {
    private final ClassB classB; //의존성 주입을 통해 import문을 제거하여 의존성 제거

    public void methodA() {
        System.out.println("Method A");
        classB.methodB();
    }
}

@Component
@RequiredArgsConstructor
public class ClassB {

    private final ClassA classA;

    public void methodB() {
        System.out.println("Method B");
        classA.methodA();
    }
}

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

  • Spring과 같은 프레임워크를 사용한다면 가장 쉽고 많이 사용하는 방법입니다.
  • 위와 같이 스프링 컨테이너에 Bean을 등록하여 의존성 주입을 통해 Cyclic Dependency를 해결할 수 있습니다.

3.3 중간 계층 도입

  • 중간 계층을 도입하여 CylicDependency를 제거할 수 있습니다. 예를 들어, ClassA와 ClassB가 직접적으로 의존하지 않고 Mediator 클래스를 통해 통신하도록 할 수 있습니다.
public class Mediator {
    private ClassA classA;
    private ClassB classB;

    public Mediator() {
        this.classA = new ClassA(this);
        this.classB = new ClassB(this);
    }

    public void mediateAtoB() {
        classB.methodB();
    }

    public void mediateBtoA() {
        classA.methodA();
    }
}

public class ClassA {
    private Mediator mediator;

    public ClassA(Mediator mediator) {
        this.mediator = mediator;
    }

    public void methodA() {
        System.out.println("Method A");
        mediator.mediateAtoB();
    }
}

public class ClassB {
    private Mediator mediator;

    public ClassB(Mediator mediator) {
        this.mediator = mediator;
    }

    public void methodB() {
        System.out.println("Method B");
        mediator.mediateBtoA();
    }
}

 

  • ClassA와 ClassB는 서로 직접적으로 의존하지 않으며, Mediator 클래스가 통신을 중재합니다.

패키지명은 소문자로 구성한다

  • 과제를 진행하면서 패키지 이름을 ladderConnectOrder 로 작성했었습니다.
  • Google 자바 스타일 가이드 5.2.1 에 따르면 패키지명은 소문자와 숫자로만 구성하도록 가이드하고 있기 때문에 가능하면 패키지명은 소문자와 숫자로만 구성하는 것이 좋습니다.

원시 타입의 값을 객체로 포장할 때 필드의 이름은 Value로 하자.

  • 원시 타입을 포장한 클래스의 이름과 필드 이름이 같은 경우 메소드명이 어색해지는 경우가 존재합니다.
public class Point {

    private final boolean point;
    
    public Point(boolean point) {
        this.point = point;
    }

    public boolean isPointFalse() {
        return !point; // 클래스 이름의 Point인지 필드의 point인지 헷갈린다.
    }

    public boolean isPointTrue() {
        return point;
    }

    public Point decideNextPoint() {
        if (!point) {
            return new Point(RandomGenerator.createRandomBoolean());
        }
        return new Point(false);
    }
}
  • 예를 들어, 위와 같이 boolean을 자료형으로 가지는 point 필드를 포장하였을 경우 클래스명도 Point이고 필드의 이름도 point이기 때문에 헷갈리는 경우가 존재합니다.
  • 그렇기 때문에 위와 같이 원시값을 포장하는 경우 필드의 이름을 value 로 변경해 줄 경우 가독성이 좋아집니다.

자바 오픈소스의 원시 포장 값의 이름도 value로 사용중이다!

  • 실제로 자바의 AtomicInteger 클래스 또한 원시값을 포장한 클래스인데 필드 이름이 value로 되어있습니다. 이러한 네이밍은 자바의 오픈소스를 따라가는 것이 좋은 방법이라고 생각합니다.

클래스 내의 변수 선언 위치는 클래스 변수, 인스턴스 변수 순서다.

public class MaxRegistrationCount {
		//메모리에 먼저 로딩되는 클래스 변수를 최상단에 위치한다.
    private static final Comparator<RegistrationCount> COMPARATOR = Comparator.comparingInt(RegistrationCount::getValue);
    //이후에 인스턴스 변수를 위치시킨다.
    private final RegistrationCount registrationCount;
}
  • 클래스에서 변수를 선언할 때 클래스 변수와 인스턴스 변수 중 클래스 변수를 먼저 선언하는 것이 가독성 측면에서 좋습니다.
  • 클래스 변수는 공유되는 변수이기 때문에 클래스를 보게 될 때 가장 먼저 확인하는 것이 유지보수, 가독성 측면에서 좋기 때문입니다. (관례상으로도 클래스 변수를 먼저 선언한다고 하네요!)

후기

  • 5월 초에 NextStep 마지막 과제를 끝냈었는데 후기를 6월 초에 작성하게 되었네요. 미루지 않도록 노력하지만 업무 과제와 새롭게 공부할 내용들이 많아 바로 작성하지를 못했네요. (핑계입니다)
  • 한 달 동안 코드스타일에 극적인 변화가 있었던건 아니지만 안 좋은 습관과 보지 못했던 인사이트들을 많이 얻을 수 있었습니다.
  • 마지막 배웠던 점을 정리하면서 이전 과제들의 MR을 다시 보면서 느꼈던 점은 대부분의 피드백들이 이펙티브 자바에 근거한다는것입니다.
  • 자바의 오픈소스에 많이 기여한 사람이 쓴 책이다 보니 주장과 근거가 타당하여 자바 개발자에게는 성경(?)과도 여겨지는 책입니다.
  • 그렇기 때문에 해당 과정이 끝나고 나서는 이펙티브 자바를 이어서 공부하게 될 거 같습니다.
  • 자바 개발자로서 취업을 준비하거나 취업한 지 얼마 안 된 개발자라면 듣기에 좋은 교육인 것 같습니다.