uncategorized

실전 자바 소프트웨어 개발을 읽고 재밌었던 부분 모음

실전 자바 소프트웨어 개발을 읽고 몇가지 재밌는 포인트를 적어본다.

Q. 유지보수에 유리한 코드는 어떤 코드인가?

특정 기능을 담당하는 코드를 빠르게 찾을 수 있어야 한다. 코드가 어떤 일을 수행하는 지 쉽게 파악할 수 있어야 한다. 새로운 기능을 추가하기 쉽고 기존의 기능을 제거하기 쉬워야 한다. 캡슐화가 잘 되어 있어야 한다. 사용자가 내부 구현을 몰라도 쉽게 이해할 수 있어야 하고 쉽게 다른 기능으로 바꿀 수 있어야 한다.

Q. 코드 중복은 왜 안좋은가?

코드 중복은 해당 기능 변경이 일어나면 변경 작업을 여러 곳에서 해야 한다. 이 과정에서 빼먹는 곳이 있다면 버그가 발생한다.

Q. 응집도가 무엇인가? 왜 중요한가?

응집도는 메서드나 클래스 혹은 모듈 속에 있는 구성 요소들이 얼마나 연관있는지를 가르킨다. 코드를 쉽게 찾고 이해하고 사용할 수 있게한다.

Q. 클래스 수준 응집도를 어떤 기준으로 높일 수 있나?

기능, 정보, 유틸리티, 논리, 순차, 시간 등 기준으로 클래스를 분리할 수 있다. 기능이 비슷한 메서드를 모아서 할 수 있다. 함께 사용하는 메서드가 같은 클래스에 모여 있어서 찾기 쉽고 이해하기 쉽다. 하지만 기능이 세세하게 나눠질 경우 클래스파일이 매우 많아져 더 이해하기 어려워질 수 있다.

정보를 기준으로 메서드를 모을 수 있다. 특정 객체를 처리하는 메서드를 모으는 방식이다. 하지만 이 경우 여러 기능이 한 클래스에 모이게 되고 일부 기능만 사용하고 싶어도 해당 클래스에 의존하게 된다.

유틸리티는 어디에 포함시키기 어려운 메서드를 모으는 방식이다. 이렇게 모인 메서드들은 서로 연관성이 낮기 때문에 주의해야 한다.

논리는 비슷해보이는 메서드를 모은다. 책에서 나온 예시는 문자열을 입력받아 특정 객체로 파싱하는 로직이 있는데, 문자열의 형식이 CSV, JSON 등 다양하므로 이에 따른 다양한 파싱 로직을 한 클래스에 모은 방식이다. 이렇게하면 다양한 로직이 모이게 되는 것이므로 SRP에 위반하게 된다.

하나의 비즈니스 로직의 순차적인 진행을 묶을 수 있다. 로직의 진행을 이해하기 쉬울 수는 있으나 다양한 기능이 하나의 단위로 묶이게 되므로 SRP를 위배하게 된다.

마지막으로 시간 응집 클래스는 특정 시점에 실행되어야 하는 기능을 모은 클래스다. 책에서는 특정 작업 이전과 이후에 해야할 작업들을 모으는 예시를 들고 있다.

Q. 개방 폐쇄 원칙이 무엇이고 구체적으로 어떤 예시를 들 수 있나?

개방 폐쇄 원칙은 기존의 기능을 확장할 때 다른 코드를 변경하지도 않고 가능해야 한다는 의미다. 책에서는 은행 거래 내역 찾기를 예시로 들었다.

다음은 특정 금액 이상의 거래 내역 찾기이다.

1
2
3
4
5
6
7
8
9
public List<BankTransaction> findGreaterThanEqual(final int amount) {
final List<BankTransaction> result = new ArrayList<>();
for (final BankTransaction bankTransaction : bankTransactions) {
if (bankTransaction.getAmount() >= amount) {
result.add(bankTransaction);
}
}
return result;
}

비슷한 로직을 가진 특정 월에 거래된 거래 내역 찾기이다.

1
2
3
4
5
6
7
8
9
public List<BankTransaction> findInMonth(final Month month) {
final List<BankTransaction> result = new ArrayList<>();
for (final BankTransaction bankTransaction : bankTransactions) {
if (bankTransaction.getDate().getMonth() == month) {
result.add(bankTransaction);
}
}
return result;
}

여기에 특정 월에 거래되었고, 특정 금액 이상의 거래 내역을 찾으려고 한다면 비슷한 메서드를 또 추가해줘야 할 것이다. 이 예시의 거래 내역 찾기 기능은 확장에 열려있지 않다. 그리고 위 예시는 비슷한 로직을 가진 여러 개의 메서드가 생기는, 즉 중복 코드를 가지기 때문에 중복된 부분에 변경이 생길 경우 여러 곳을 변경해야 한다. 즉 변경에 닫혀있지 않다.

이를 위해서 메서드들마다 중복되는 코드와 그렇지 않은 코드를 분리하자. 위 예시에서는 거래 내역을 순회하는 로직이 있고, 거래 내역이 조건에 맞는 지 확인하는 로직이 있다. 거래 내역 순회 로직은 각 메서드마다 중복되고, 거래 내역 조건 확인하는 로직은 각 메서드마다 달라진다. 문제는 이 두 로직이 결합되어 있어서 순회 로직이 거래 내역 조건 확인 로직이 추가될 때마다 중복되어 작성된다는 점이다.

이를 위해서는 두 로직의 결합을 제거하기로 한다. 우리는 인터페이스로 로직을 분리한다.

1
2
3
4
@FunctionalInterface
public interface BankTransactionFilter {
boolean test(BankTransaction bankTransaction);
}

이제 조건 맞는지 확인하는 로직을 해당 인터페이스로 추상화할 수 있다. 인터페이스로 순회로직이 검증 로직들과 분리되었다. 매개변수로 함수형 인터페이스의 구현체 객체를 전달하거나 람다를 전달하면 된다.

1
2
3
4
5
6
7
public List<BankTransaction> findTransactions(final BankTransactionFilter filter) {
final List<BankTransaction> result = new ArrayList<>();
for (final BankTransaction bankTransaction : bankTransactions) {
if (filter.test(bankTransaction) {
result.add(bankTransaction);
}
}

Q. 인터페이스를 잘게 분리하는 게 좋나? 큰 인터페이스는 어떤 문제가 있나?

한 인터페이스가 많은 연산을 제공하면 그 만큼 인터페이스가 변경될 여지가 많아진다. 인터페이스가 변경될 때마다 모든 구현체를 변경해줘야 한다. 변경이 많아지면 문제가 생길 여지가 많아진다. 그리고 인터페이스에 특정 도메인 내부 구현이 드러나면 안된다. 구현이 변경되면 인터페이스에도 변경이 생기고 결과적으로 모든 구현체에 변경해야 할 일이 생긴다.

하지만 인터페이스를 극단적으로 잘게 나누면 기능이 여러 인터페이스로 분산되서 필요한 기능을 찾기 어려워지는 문제가 생길 수 있다.

Q. 명시적 API VS 암묵적 API

개방 폐쇄 원칙으로 하나의 메서드로 기능을 쉽게 확장할 수 있게 했던 findTransactions() 메서드와 같이 유연하지만 처음에 사용하기 어려운 암묵적 API와 구체적으로 어떤 작동을 하는 지 드러나는 findTransactionsGreaterThanEqual() 같은 메서드를 명시적 API라고 한다. 흔하게 사용하는 경우는 명시적 API를 추가해놓는 게 편할 수 있다.

Q. CheckedException과 UncheckedException 의 차이는 뭘까?

CheckedException은 API 사용자가 회복해야 할 예외다. 시그니처에 드러내거나 Try catch로 처리해줘야 한다. 반면 UncheckedException은 프로그램 실행 중 언제든지 발생할 수 있는 예외다.

이 두가지를 선택하는 기준은 응용 프로그램을 회복시켜야 하는가(혹은 회복 시킬 수 있는가?)이다. 일시적으로 문제가 발생한 경우는 API 사용자가 회복을 하려고 하는 것보다 잠시 뒤에 다시 재요청하는 게 나을 것이다. 그리고 비즈니스 로직 검증 시 발생한 문제는 불필요한 Try catch나 시그니처에 노출하지 않도록 하기 위해서 unchecked로 하는 게 좋다. 마지막으로 시스템 예외가 발생한 경우 API 사용자가 회복 시킬 수 있는 상황이 아닌 경우도 있다. 이럴 때도 unchecked로 처리하는 것이 좋다. 결론은 대부분의 unchecked 예외로 처리해서 불필요한 try catch나 메서드 시그니처에 노출하지 않도록 하는 게 좋다.

Q. 검증 코드는 어디에 두는 것이 좋을까? 객체 생성 시 VS 검증자

책에서는 검증자를 추천한다. 검증 로직을 재사용할 수 있고 로직을 독립적으로 테스트할 수 잇고 SRP를 지키기 때문이다.

Q. 예외로 흐름을 제어하면 안되는 이유가 무엇인가?

예외를 처리하는 불필요한 try catch가 코드 가독성을 떨어트린다. try catch는 흐름 제어가 아닌 예외나 오류 처리를 위한 코드라서 흐름 제어에 사용하면 코드 이해하기가 어려워진다. 마지막으로 예외를 생성했을 때 스택 트레이스 생성, 보존과 관련된 부담이 생긴다.

Q. 예외 대신에 사용할 수 있는 기능은 무엇인가?

Null을 반환하도록 할 수 있다. 다만 어느 시점에서 Null이 반환됐는 지 추적하기가 어렵다. 그리고 API 호출마다 null 체크를 해야 하는 번거로움이 생기고 Null 만으로는 그 어떤 정보도 얻을 수 없어서 추천하지 않는다.

Null 오브젝트 패턴을 사용할 수 있다. 반환되어야할 인터페이스를 구현하되 바디가 비어있는 형식을 가진 게 Null 오브젝트의 특징이다. 사용자에게 제공하고 싶은 내용을 담을 수 있고 NPE나 null 체크를 하지 않아도 된다. 다만 null 오브젝트는 발생 시 아무 단서를 주지 않기 때문에 문제를 파악하기가 어렵다.

그 외에 자바 8의 Optional을 사용할 수 있다.

Q. 빌드 도구를 왜 사용하는가?

프로젝트를 실행하기 위해서는 컴파일해야 한다. 이때 여러 파일을 컴파일하는 명령어를 기억하기 힘들다. 여러 패키지를 컴파일하려면 어떤 명령어를 사용해야 할까? 다른 라이브러리 의존성은 어떻게 관리할 것인가? JAR와 같이 특정한 형식으로 어떻게 패키징할까 등 다양한 문제가 있다. 프로그램을 실행할 때마다 이런 문제를 고민하기 보다는 스크립트를 통해 모든 명령어를 자동화하면 훨씬 편한다. 다음은 빌드 도구 장점을 정리한 내용이다.

  • 응용 프로그램 빌드하고 실행하는 표준적인 작업 설정
  • 저수준 설정과 초기화에 들이는 시간을 절약해서 개발에 집중 가능
  • 설정과 빌드 과정에서의 오류 범위를 줄임

Q. 리스코프 치환 원칙에 대해 설명해보라

리스코프 치환 원칙은 크게 네가지 원칙으로 쉽게 설명할 수 있다.

  • 하위형식에서 선행조건을 더할 수 없다.
    자식이 부모보다 더 많은 선행조건을 요구할 수 없다.
  • 하위형식에서 후행조건을 약화시킬 수 없다.
    후행조건은 코드를 실행한 다음에 만족해야 하는 규칙이다. 자식은 부모의 후행조건을 반드시 만족해야 한다.
  • 슈퍼형식의 불변자는 하위형식에서 보존
  • 히스토리 규칙
    부모가 허용하지 않은 상태 변화를 허용해서는 안된다. (부모가 불변 객체면 자식도 이를 준수)

Q. 클라이언트 서버 모델에서 서버가 먼저 데이터를 전달하고 싶으면 어떻게 하는가?

푸시 기반 통신을 사용한다. 리액티브 혹은 이벤트 주도 통신이라고도 한다. 푸시 기반 통신에서는 작성자(publisher)가 이벤트 스트림을 구독하는 상대들에게 데이터를 전달한다. 푸시 기반 통신은 일대다 통신을 지원하고 복잡한 컴포넌트들과 통신해야 할 때 유리하다.

풀 기반 통신은 푸시 기반 통신의 반대다. 클라이언트가 원하는 데이터를 먼저 서버에게 요청하고 응답받는 방식이다.

Q. 저장소 패턴이란 무엇인가? 레포지토리의 책임은 무엇인가?

저장소 패턴은 비즈니스 로직과 저장소 백엔드 간의 인터페이스를 정의한다. 저장소 패턴은 저장소 백엔드를 추상화해서 저장소를 다른 방식으로 구현해도 비즈니스 로직에는 영향이 가지 않도록 한다. 저장소 패턴을 사용하면 도메인 모델 데이터로 매핑하는 로직을 중앙화 할 수 있다.

Share