최근 자바로 간단한 프로그래밍을 하며
Object-Oriented Programming과 Clean Code, Test Driven Development에 관심이 많다.
클린 코드를 공부하며 직접 자바로 구현한 코드를 소개해 보려고 한다.
가독성이 좋고 이해하기 쉬우며, 유연성이 높고 재사용성이 좋은 코드이다.
사실 TDD의 선구자이며 SOLID의 창시자인 Robert C. Martin의 저서 이름이다.

내가 생각했을 때 클린 코드의 장점은 '좋은 코드'라는 추상적인 평가를 분명한 말들로 표현하며 코드의 질을 향상시키고 코드로 일관적인 소통을 할 수 있다는 것이다.
실제로 내가 이게 맞지 않나? 싶었던 좋은 코드의 느낌들이 <클린 코드>에 설명되어 있어서 신기했다
많은 사람들이 언급하듯 가독성이 좋고 이해하기 쉬운 코드는 팀의 생산성을 향상시킨다!
재사용성이 좋은 코드의 장점은 말할 것도 없다.
공부를 하다 보니 OOP를 지키는 것과 클린 코드를 지향하는 것은 결이 비슷하다.
객체 지향의 장점이 아래와 같기 때문이다.
이제 OOP를 준수하고 클린 코드를 지키기 위해 노력한 과정을 소개하겠다.
✦ 정적 팩토리 메서드를 바로 떠올릴 수 있다.
정적 팩토리 메서드는 스프링부트를 사용할 때 아래와 같이 엔티티 클래스와 DTO 클래스 간 변환에서 자주 사용해왔다.
@Getter
@Entity
@NoArgsConstructor
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column
private String description;
@Builder
public Post(String title, String description) {
this.title = title;
this.description = description;
}
}
@Builder
@Getter
public class PostResponse {
private Long id;
private String title;
private String description;
public static PostResponse fromEntity(Post post){
return PostResponse.builder()
.id(post.getId())
.title(post.getTitle())
.description(post.getDescription())
.build();
}
✦ 메서드로 캡슐화하기
이건 자바로 간단한 프로그래밍을 구현하던 중, getter를 쓰지 않고 응답을 담당하는 클래스로 어떻게 변환할지 고민하다가 객체 생성을 캡슐화한 메서드를 만들었던 코드이다.
public class Order {
private final Map<Menu, Integer> orders;
// 생략
public ItemResponse convertToResponse() {
return new ItemResponse(orders);
}
(2024.8.7) DTO와 도메인 클래스 간의 의존은 댓글을 참고해주세요.
✦ enum**에서의 활용
OOP와 클린코드에 대한 레퍼런스를 찾다가 현업에서 enum을 어떻게 활용하고 있는가에 대한 게시글을 읽게 되었다. enum의 이점을 확실히 알게 되고 아래와 같이 활용했다.
public enum Day {
MONDAY(4),
TUESDAY(5),
WEDNESDAY(6),
THURSDAY(0),
FRIDAY(1),
SATURDAY(2),
SUNDAY(3);
private int offset;
Day(final int offset) {
this.offset = offset;
}
public static Day findByDate(final int date) {
final int offset = date % 7;
return Arrays.stream(Day.values())
.filter(day -> day.offset == offset)
.findAny().get();
}
}
enum에 연관되는 상수(여기에서는 요일)을 저장하고 각 상수로 인스턴스를 만들 때는 외부 로직이 아니라 내부 static 메서드로 반환한다. 객체 생성에 이름을 부여하며 목적을 확실히 알 수 있고, 캡슐화로 도메인 클래스의 로직을 한 곳에서 관리하여 유지보수성을 향상시킬 수 있다.
테스트 코드를 작성하기 쉬운 구현 코드를 작성한다.
테스트를 하기 위해 구현 코드에 메서드를 추가(getter등)하는 것과는 다른 말이다. (이건 당연히 좋지 않다.)
좋은 레퍼런스가 있다.
메서드 시그니처를 수정하여 테스트하기 좋은 메서드로 만들기
나는 Postman 등으로 로컬에서 API 테스트를 하는 과정이 번거로워 테스트 코드에 입문했다. (....) 테스트하기 쉬운 코드는 그때 그때 오류를 찾아낼 수 있는 것 외에도 아래와 같은 장점이 있다.
로또에 대한 도메인 클래스를 예시로 보자.
public class Lotto {
private final List<Integer> numbers;
public Lotto() {
this.numbers = pickUniqueNumbersInRange(1, 45, 6);
}
}
이 로또 클래스의 numbers를 사용하는 어떤 테스트도 저 랜덤 번호의 값을 알고 활용할 수 없다.
아래와 같이 바꾼다면 로또 번호를 내가 설정해서 테스트할 수 있다.
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) {
validateSize(numbers);
validateNumber(numbers);
validateDuplicate(numbers);
this.numbers = numbers;
}
}
생성자에 있는 validation 함수는 각각 로또 번호 개수, 번호의 범위(1부터 45까지의 정수), 중복을 확인한다. 해당 로또 클래스는 랜덤 번호를 생성할 때 뿐 아니라 다양한 서비스 로직에서 활용될 수 있고, 로또 번호라는 특징 및 제한 사항을 매번 점검하는 로직이 필요하다.
로또 클래스를 사용할 때마다 외부 로직에서 복잡하게 검증하는 것보다, 로또 클래스 내부에서 검증하는 것이 더 편하고 직관적이다.
테스트하기 쉬운 코드를 작성하다가 검증하기 쉽고 재사용성이 높은 코드가 되었다! (^_^)
getter와 setter를 남발하지 말자. 클래스 내부에 로직을 구현해서 단일 책임의 원칙을 지키고, 상태와 기능을 한 곳에서 관리하자. 위 로또 클래스로 또 예시를 들어보면
public class Lotto {
private final List<Integer> numbers;
// 생략
public boolean contains(Integer number) {
return numbers.contains(number);
}
public int countMatching(Lotto other) {
return (int) numbers.stream()
.filter(num -> other.contains(num))
.count();
}
}
위와 같이 두 로또의 일치하는 번호 개수를 구할 수 있다.
(이렇게 OOP를 일일히 생각하며 구현한게 이번이 처음이라, 저 countMatching 메서드 생성하고 좀 뿌듯했다 ㅎㅎ..)
서술적이고 직관적인 메서드 이름, 클래스와 메서드의 크기를 줄이고 필드의 개수를 줄이기 등등.. 역시 좋은 레퍼런스들이 많다.
네이버클라우드개발자의 좋은 코드란 무엇일까?
아직 부족한 점이 많지만 점점 깨끗해지는 코드를 보면 기분이 너무 좋다!
안녕하세요.
포스트를 쭉 보다가 여기까지 왔네요.
필자분은 이미 아실것 같지만, 이 포스트를 처음보는 사람들이 볼 수 있도록 댓글을 남깁니다.
Entity 와 DTO 합치는 코드는 지양하시는 게 좋습니다.
현업에서도 간혹 저런 코드를 사용하시는 분이 있는데, DTO 는 최대한 POJO 로 두시는 걸 추천드립니다.
convert 처리는 objectmapper 를 이용하시되, util 클래스를 별도로 만드셔서, 매핑에러 공통 처리 등을 구현하고 스태틱 메서드로 이용하거나, 빈으로 주입 시키는 방식이 스프링 철학에 어울립니다.