📚 Study

[프로젝트] 코드리뷰 코멘트 정리

date
May 20, 2024
slug
devdevdev-code-review-summary
author
status
Public
category
📚 Study
tags
프로젝트
구현
summary
댑댑댑 프로젝트를 진행하며 나눴던 코드리뷰 코멘트를 정리해 보았다. 리뷰에서 끝나는 것이 아니라 실제로 내 것으로 만들어야 한다.
type
Post
thumbnail
fc78d6c5-5cdb-4a1e-8a6c-9b115e456b88_934x499.jpeg
 
댑댑댑 프로젝트를 진행하면서 세웅님과 나눴던 코드리뷰를 정리할 필요성을 느꼈다. 왜냐하면.. 열띤 리뷰 끝에 Merge를 완료하고 나면 막상 어떤 부분에서 어떤 주제로 토론을 했고, 어떤 의사결정을 내렸는지 모두 까먹어버리기 때문이다..🥹
 
1차 릴리즈 전에, 그동안 나눴던 코드리뷰 코멘트들을 정리해보고 2차 개발에서 같은 내용의 대화와 코드 수정을 반복하지 않을 수 있도록 해보겠다!!
 
 
notion image
 
 
 
 

접근제어자는 기본 중의 기본이다

클래스 안에서만 사용하는 필드, 메서드는 꼭 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 경로를 *로 표현한다면 우리는 코드가 표현하고 있는 것을 읽을 수 없다.
 

불변객체를 활용하자 (🐱)

리스트 등의 객체를 반환하는 경우, setXXX(), add() 등의 메소드로 인해 해당 데이터가 변경되지 않도록 주의해야 한다.
  • Lists.newArrayList()를 반환한다면 변경에 열려있다.
  • 변경 불가능한 리스트를 생성하는 방법
    • 1. Collections.unmodifiableList()
      • 원본 List를 readOnly 뷰로 감싼 형태
      • 변경과 관련된 메소드를 호출하면 예외 발생
      • 그러나 원본 리스트의 변경을 막을 수 없기 때문에 완전한 불변 리스트라고 할 수 없다(참고)
      1. List.copyOf()
          • 원본 리스트와 동일한 객체를 바라보지 않도록 새로운 리스트를 복제하여 사용
          • Java 10 이상에서 사용
      1. Collectors.toUnmodifiableList()toList()
          • 스트림을 사용하여 불변리스트 생성
          • toList()는 Java 16 이상에서 사용
          • 두 메소드의 차이: collect(Collectors.toUnmodifiableList())는 input list에 null 요소를 허용하지 않고, Stream.toList()는 null 요소를 허용함
이 중 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