📚 Study
[프로젝트] 코드리뷰 코멘트 정리
댑댑댑 프로젝트를 진행하면서 세웅님과 나눴던 코드리뷰를 정리할 필요성을 느꼈다. 왜냐하면.. 열띤 리뷰 끝에 Merge를 완료하고 나면 막상 어떤 부분에서 어떤 주제로 토론을 했고, 어떤 의사결정을 내렸는지 모두 까먹어버리기 때문이다..🥹
1차 릴리즈 전에, 그동안 나눴던 코드리뷰 코멘트들을 정리해보고 2차 개발에서 같은 내용의 대화와 코드 수정을 반복하지 않을 수 있도록 해보겠다!!

접근제어자는 기본 중의 기본이다상수는 멤버 필드 위에 적는다 (🐱)@EventListener와 @PostContruct (🐱)환경변수 관리는 @ConfigurationProperties(prefix = "xxx") 를 사용하자 (🐱)Entity 생성자에 id 넣지 않도록 주의하자 (🐱)변수명에 자료형을 명시하지 말자 (🐱)Setter를 외부에서 사용하지 않으면 @Setter(AccessLevel.PRIVATE) 접근자로 제어하자 (🐱)List<xxxResponse>의 변수명은 xxsResponse로 명명하자 (🐱)Optional은 비싸다 (🐱)패키지 구조 설계를 고민하자 (🐱)@Valid를 사용하여 RequestBody 객체를 검증하자 (🐱)import 경로를 * 으로 표현하는 것을 지양하자 (🐱)불변객체를 활용하자 (🐱)테스트코드에서는 Entity의 create() 메소드에 의존하지 말고 빌더를 사용한 메소드를 따로 만들자 (🐱)문자열의 empty를 확인할 때는 StringUtils.hasText() 메서드를 사용하자 (🐱)문자열 join할 때는 Collectors.joining() 메서드를 사용하자 (🐱)시간/날짜 관련 로직을 구현할 때는 TimeProvider를 사용하여 시간/날짜를 외부에서 인자로 받아 사용할 수 있도록 구현하자 (🐱)
접근제어자는 기본 중의 기본이다
클래스 안에서만 사용하는 필드, 메서드는 꼭
private
으로 생성해주는 것 잊지 말자~접근제어자를 신경쓰지 않는 자바개발자는 자바개발자라고 할 수 없다,,
상수는 멤버 필드 위에 적는다 (🐱)
public final static String nickname = "댑댑이"; public final static String email = "test@devdevdev.com"; public final static Role role = Role.ROLE_USER; public final static SocialType socialType = SocialType.KAKAO; private final MemberRepository memberRepository;
@EventListener와 @PostContruct (🐱)
SpringBoot 실행 후 초기화 코드를 넣는 방법
@PostConstruct
는 AOP 이전에 실행되기 때문에 Transactional 등을 사용할 수 없어서, 데이터 Insert에는 권장되지 않는다.
@PostConstruct
는 주로 클래스 초기화에 사용되고, 더미 데이터 삽입 등은@EventListener
를 사용한다.
- 코드에서 테스트계정 생성시 트랜잭션을 사용하지는 않지만, 각 어노테이션의 사용성 측면과 추후 다른 로직이 추가될 수도 있다는 점을 고려하여
@EventListener
를 사용하였다.
환경변수 관리는 @ConfigurationProperties(prefix = "xxx")
를 사용하자 (🐱)
Entity 생성자에 id 넣지 않도록 주의하자 (🐱)
@Builder private Member(Long id, String name, String job) { this.id = id; ... }
변수명에 자료형을 명시하지 말자 (🐱)
특정 변수의 자료형이 다른 자료형으로 변경된다면 변수명까지 수정해야 하는 작업이 생길 수 있다.
변수명을 지을 때는 자료형에 대한 의존성을 제거하도록 하자.
Setter를 외부에서 사용하지 않으면 @Setter(AccessLevel.PRIVATE)
접근자로 제어하자 (🐱)
List<xxxResponse>의 변수명은 xxsResponse로 명명하자 (🐱)
- xxsResponse (O)
- yyesResponse (O)
- xxResponses (X)
Optional은 비싸다 (🐱)
패키지 구조 설계를 고민하자 (🐱)
멋진 패키지 전략을 사용한다면 클래스가 많아져도 각 클래스 간에 어떤 관계가 있는지 패키지 구조만 보아도 쉽게 이해할 수 있을 것이다! 😍
@Valid
를 사용하여 RequestBody 객체를 검증하자 (🐱)
파라미터로 @RequestBody 어노테이션 옆에 @Valid를 작성하면, RequestBody로 들어오는 객체에 대한 검증을 수행한다. 이 검증의 세부적인 사항은 객체 안에 정의를 해두어야 한다.
@RestController @Slf4j public class TestController { @PostMapping("/user") public ResponseEntity<String> savePost(final @Valid @RequestBody UserDto userDto) { log.info(userDto.toString()); return ResponseEntity.ok().body("postDto 객체 검증 성공"); } } @Data public class UserDto { @NotNull private String name; @Email private String email; }
import 경로를 * 으로 표현하는 것을 지양하자 (🐱)
import 된 경로만 보아도 해당 클래스가 어떤 기능을 담당하는지 알 수 있어야 한다.
그런 관점에서, import 경로를 *로 표현한다면 우리는 코드가 표현하고 있는 것을 읽을 수 없다.
import 또한 하나의 코드임을 기억하자! (참고: Toss Slash22 강연 - 지속 성장 가능한 코드를 만들어가는 방법)
불변객체를 활용하자 (🐱)
리스트 등의 객체를 반환하는 경우, setXXX(), add() 등의 메소드로 인해 해당 데이터가 변경되지 않도록 주의해야 한다.
- Lists.newArrayList()를 반환한다면 변경에 열려있다.
- 변경 불가능한 리스트를 생성하는 방법
- 원본 List를 readOnly 뷰로 감싼 형태
- 변경과 관련된 메소드를 호출하면 예외 발생
- 그러나 원본 리스트의 변경을 막을 수 없기 때문에 완전한 불변 리스트라고 할 수 없다(참고)
List.copyOf()
- 원본 리스트와 동일한 객체를 바라보지 않도록 새로운 리스트를 복제하여 사용
- Java 10 이상에서 사용
Collectors.toUnmodifiableList()
,toList()
- 스트림을 사용하여 불변리스트 생성
- toList()는 Java 16 이상에서 사용
- 두 메소드의 차이: collect(Collectors.toUnmodifiableList())는 input list에 null 요소를 허용하지 않고, Stream.toList()는 null 요소를 허용함
1.
Collections.unmodifiableList()
이 중 3번이 가장 깔끔한 방법이라고 생각되는데요! 그 중에서도 null값을 허용하는 toList()를 적용하는 것이, NullPointerException 을 예방할 수 있는 방법일 것 같습니다!
테스트코드에서는 Entity의 create() 메소드에 의존하지 말고 빌더를 사용한 메소드를 따로 만들자 (🐱)
Entity 클래스의 create() 메소드에 의존할 경우, create 메소드에 변경이 발생하면, 모든 테스트 코드를 수정해야 할 수도 있다.
테스트코드가 비즈니스코드에 의존하지 않도록 주의하자.
문자열의 empty를 확인할 때는 StringUtils.hasText() 메서드를 사용하자 (🐱)
- ObjectUtils.isEmpty(..) 메서드는 null 여부와 빈문자열만 확인할 수 있고, 공백문자열(” “)은 확인할 수 없다.
- StringUtils.hasText(..) 메서드는 문자열이 공백으로 채워져있는지도 확인할 수 있다!
- 적절한 내부 라이브러리 메소드를 잘 사용하는 것도 개발자의 중요한 덕목이다~
// ObjectUtils.isEmpty() public static boolean isEmpty(@Nullable Object obj) { if (obj == null) { return true; } else if (obj instanceof Optional) { Optional<?> optional = (Optional)obj; return optional.isEmpty(); } else if (obj instanceof CharSequence) { CharSequence charSequence = (CharSequence)obj; return charSequence.isEmpty(); } else if (obj.getClass().isArray()) { return Array.getLength(obj) == 0; } else if (obj instanceof Collection) { Collection<?> collection = (Collection)obj; return collection.isEmpty(); } else if (obj instanceof Map) { Map<?, ?> map = (Map)obj; return map.isEmpty(); } else { return false; } }
// StringUtils.hasText() public static boolean hasText(@Nullable String str) { return str != null && !str.isBlank(); }
문자열 join할 때는 Collectors.joining() 메서드를 사용하자 (🐱)
- 짜치게 for문 돌면서 인덱스 확인하는 코드 짜지 말자… 나는 개발자다🙏
- 적절한 내부 라이브러리 메소드를 잘 사용하는 것도 개발자의 중요한 덕목이다~22222
private String concatNickname(List<MemberNicknameDictionary> words) { return words.stream() .map(word -> word.getWord().getWord()) .collect(Collectors.joining(" ")); }
시간/날짜 관련 로직을 구현할 때는 TimeProvider를 사용하여 시간/날짜를 외부에서 인자로 받아 사용할 수 있도록 구현하자 (🐱)
내부에서 LocalDateTime.now()를 직접적으로 사용하게 되면, 코드 상에 개발자가 직접 제어할 수 없는 부분을 갖게되고 그렇게 되면 테스트하기 어려운 코드가 된다.
TimeProvider를 적극 활용하여, 시간/날짜를 외부에서 인자로 받을 수 있도록 구현해보자.
Last Update: May 22, 2024