[회고] 코드개선, 테스트코드, AOP 개인과제 회고

wannabeing·2025년 4월 21일
1

SPARTA-회고

목록 보기
5/5
post-thumbnail

✅ 개요

  • 개발기간: 2025.04.15(화) ~ 2025.04.21(월)
  • 자바버전: OpenJDK 17
  • 사용기술: Spring Boot, JPA, MySQL
  • 코드 개선, AOP 적용, 테스트 코드 작성
  • 프로젝트 링크: GitHub Repository

✅ 요구사항 정의

💪 필수

  • 코드 개선 (EarlyReturn, if-else, Validation)
  • N+1 문제 동작 분석 및 @EntityGraph 기반으로 코드 개선
  • 작성된 테스트 코드 분석 및 개선

🚀 도전

  • API 로깅 (인터셉터, AOP를 활용)
  • '내'가 정의한 문제와 해결
  • 테스트 커버리지
    회원가입/로그인

✅ Keep

현재 프로젝트에서 만족했고, 앞으로의 훈련기간에서 지속하고 싶은 부분을 작성했다.

  • AOP를 학습하고 실제 프로젝트에 적용해보았다.
  • 테스트 코드의 중요성과 작성 방법을 이해하고, 직접 작성해보았다.
  • 스프링의 Bean 생명주기와 스코프에 대해 학습하며 이해했다.
  • JPQL을 찍먹해봤다.
  • 요구사항을 깊이 고민하고 구현하는 과정을 반복하고 있다.
  • 매일 코드카타를 통해 논리적인 접근을 계속해서 하고자 한다.
  • 앞으로 남은 훈련 과정에서도 주어진 과제에 적극적으로 임하며, 더 완성도 높은 결과물을 만들고싶다.

✅ Problem

현재 프로젝트에서 어려웠던 점과 아쉬웠던 점을 작성했다.

Q1. ==null 사용은 지양하는게 맞지 않나요? Optional을 쓰면 되잖아요!

A) 상황에 따라 다르고, 각각의 장단점을 이해해보자!

무의식적으로 ==null 쓰는거 자체를 코드를 작성하는데 있어 피해야 된다고 생각했다.
보통 해당 코드를 간단하게, 급하게 사용해서 그런가..?

==null 특징

  • null은 참조형 변수가 가르키는 객체의 주소가 없을 때 발생한다.
  • 값의 유무를 직관적으로 확인할 수 있어 가독성이 좋고 경제적이다.
  • 별도의 객체 생성 없이 단순히 존재 유무만 확인할 때 가볍고 빠르다.

Optional 특징

  • 다른 클래스의 메서드 값이 null일 경우, Optional로 감싸 반환하여 사용한다.
  • 다양한 기능들을 제공하고 활용하여 가독성을 높일 수 있다.
  • NPE를 방지하는 데 유용하다.
  • 만능이 아니므로, 적절한 상황에 사용하는 것이 중요!
  • 호출자한테 책임전가를 하는 것이고, 작업비용이 더 들어간다!

즉, Optional을 사용하면 ==null보다 많은 기능들을 제공하고, 그만큼 비용이 든다는 말이다.


todo.getUser().getId();

위에 코드는 메서드체이닝(고리처럼 메서드를 연결하여 호출)이라고 하는데,
위와 같은 상황은 NPE에 취약하다.
디버깅이 어렵기 때문에 어디서 null인지 찾는 시간이 많이 소요된다.

💡 "이걸 어디서 처리해야 될까?" 라는 질문을 해봤나??

❗️ null 체크를 뭘로 해야되요? 가 아니라 서비스 레이어에서 해당 로직이 존재하는지 맞나요? 라는 질문을 받았다.

Todo 엔티티 자체에서 작성자가 있는지 판단해서 아이디를 꺼내주는 메서드를 제공하는 게 더 나은 설계가 아닐까 하는 말씀을 주셨다!

정리하자면

null이 들어올 수 있는 상황을 애초에 구조적으로 막는 것,
그리고 그 책임을 어디에 둘지 고민하는 것이 정말 중요하다는 걸 느꼈다.

단순히 "== null은 나쁘다", "Optional을 써야 한다"가 아니라
NPE가 발생할 가능성, 디버깅의 난이도, 성능에 주는 영향, 가독성, 책임 위치 등등..
모든걸 따져보고 각 상황에 맞게 선택하는 게 더 현명한 접근이라는 생각이 들었다!

나는 항상 "정답"을 찾으려는 경향이 있었던 것 같다.
근데 이번에 받은 피드백을 통해,
정답보단 "논리적인 근거를 바탕으로 결정하는 것"이 더 중요하다는 걸 느꼈다.
앞으로도 이런 고민을 통해, 내 코드가 더 설득력 있고 견고해졌으면 좋겠다는 생각이다!


Q2. Bean Scope 중에 프로토 타입은 실제로 사용되나요? 언제 사용될까요?

A) 자주 사용되진 않고, 개념만 알고 있어도 충분해요!

Bean의 생명주기와 범위 정리

✅ Bean Prototype Scope는

스프링에서 기본 스코프는 싱글톤이다. 애플리케이션 컨텍스트당 하나의 인스턴스만 생성되어 공유된다. 반면, 프로토타입 스코프는 매번 요청할 때마다 새로운 인스턴스를 생성한다. 따로 인스턴스 삭제를 책임지지 않기 때문에 GC를 통해 삭제된다.

✅ Bean ProtoType Scope가 사용되는 경우

  • 상태를 가지는 객체: 각 인스턴스가 고유한 상태를 유지해야 하는 경우.
    예를 들어, 사용자별 설정이나 세션 관련 데이터를 담는 객체 (장바구니?)

  • 매 요청마다 새로운 인스턴스가 필요한 경우: 예를 들어, 폼 요소를 동적으로 생성하거나, 각 요청에 따라 다른 설정이 필요한 경우.

  • 런타임에 파라미터를 전달해야 하는 경우: ApplicationContext.getBean()을 사용하여 런타임에 파라미터를 전달하고 새로운 인스턴스를 생성해야 할 때.

기본값인 싱글톤으로도 대부분 구현이 되기 때문에 프로토타입 스코프는 자주 사용되지는 않는다고 한다. 지금은 스코프의 종류들과 특징들만 알고만 있어도 충분하다는 말씀을 주셨다!


Q3. SecurityContextHolder와 RequestContextHolder는 다른걸까요?

A) 완전 다릅니다! 각각의 차이점을 알아야됩니다!

AOP 맛만 볼까..?

AOP로 로깅구현을 하는 도중에 RequestContextHolder를 활용하면
HTTP 요청 데이터를 전역에서 추출할 수 있다는 것을 알았다.

RequestContextHolder.getRequestAttributes()를 통해 데이터를 얻을 수 있다.
하지만 반환 타입이 인터페이스라서 구현체가 필요하다!

또한 우리가 익숙하게 사용하는 HttpServletRequest로 사용하려면ServletRequestAttributes로 다운캐스팅을 해줘야 한다.

그러다 세션을 듣는 중에 SecurityContextHolder라는 타입을 알게 되었고, RequestContextHolder와 비슷하게 생겨서 이것도 HTTP 요청에 대한 구현체인지에 대해 여쭤봤다.

SecurityContextHolder는
SpringSecurity 프레임워크를 사용하면서 현재 요청 및 인증과 관련된 정보를
접근하려고 할 때 사용하는 클래스이다. ThreadLocal에 정보를 저장하고 있다.

RequestContextHolder는
현재 HTTP Request에 대한 정보를 접근하려고 할 때 사용하는 클래스이다.
ThreadLocal에 정보를 저장하고 있다.

💡 동작방식은 비슷하지만, 쓰임새가 전혀 다르다!!

스프링은 HTTP 요청마다 하나의 쓰레드를 사용하고, 이 쓰레드가 전담하게 한다.
쓰레드에 관련된 정보들을 ThreadLocal에 저장해서 응답하기 전까지 어디서든 사용할 수 있도록 도와준다.

RequestContextHolder의 경우, 비교적 무겁고 코드를 추가하기에도 부담스럽다.
따라서 상황에 따라 ThreadLocal을 다룰 수 있다면, 커스텀하여 사용할 수도 있다!

따라서

내가 SpringSecurity 프레임워크가 갖고 있는 요청 정보를 접근하고자 한다면 SecurityContextHolder를 사용하면 되고,

내가 HTTP 요청에 대한 정보를 전체적으로 알고 싶다면 RequestContextHolder를 사용하면 된다.

그것도 아니라면 ThreadLocal을 다뤄서 CustomContextHolder를 구현할 수도 있다!


API 응답 공통화 하기

수정 전 로그인 응답

{
    "bearerToken": "Bearer ${발급된 토큰}"
}

수정 전 Todo 리스트 응답

"content": [],
"page": {
  "size": 10,
  "number": 0,
  "totalElements": 0,
  "totalPages": 0
}

응답 형식이 제각각이면 클라이언트에서는 어떤 형태가 올지 매번 체크해야 하고,
API 문서를 볼 때도 형식이 제각각이라 파악이 힘들고,
공통 로직 적용도 어렵다. 따라서 공통 응답 DTO를 만들기로 했다!

✅ 이러한 장점이 있다.

  • 클라이언트는 data만 신경 쓰면 된다
  • timestamp, status, message 등 부가정보로 디버깅/로깅에 용이하다.
  • 향후 API 규칙이 변경되더라도 공통 포맷만 수정하면 된다.

🚀 적용한 공통 응답 DTO

@Getter
@RequiredArgsConstructor
public class SuccessResponseDto<T> {
	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
	private final LocalDateTime timestamp;

	private final int code;
	private final String status;
	private final String path;
	private final String message;

	@JsonInclude(JsonInclude.Include.NON_NULL) // ✅ null 이면 응답 JSON 에서 생략됨
	private final T data;
  • timestamp: 응답 시간을 알려준다.
  • code: 200, 201 등의 HTTP 상태 코드
  • status: HTTP 상태 코드의 설명 문자열
  • path: 클라이언트 요청 URI
  • data: 각 요청별 응답 데이터 (존재하지 않을 수도 있다.)

위와 같이 공통 응답 DTO를 만들었고, 아래와 같이 변경되었다.

✅ 공통 응답 구조 적용한 로그인 응답

{
    "timestamp": "2025-04-21 09:13:39",
    "code": 200,
    "status": "OK",
    "path": "/auth/signup",
    "message": "로그인에 성공하였습니다.",
    "data": {
        "bearerToken": "Bearer ${발급된 토큰}"
    }
}

✅ 공통 응답 구조 적용한 Todo 리스트 응답

// 리스트 응답
{
    "timestamp": "2025-04-21 10:58:01",
    "code": 200,
    "status": "OK",
    "path": "/todos",
    "message": "todo 리스트를 성공적으로 조회하였습니다.",
    "data": {
        "content": [],
        "page": {
            "size": 10,
            "number": 0,
            "totalElements": 0,
            "totalPages": 0
        }
    }
}

이처럼 응답 구조를 통일하는 건
유지보수와 확장성, 디버깅 편의성까지 고려하면 작은 변화지만 꽤 큰 효과를 주는 리팩토링이었다고 생각한다.
개인적으로 공통으로 설계할 수 있는 부분을 먼저 찾아내고 적용해 나가는 것이
작업의 효율이 올라간다고 생각한다!


테스트 코드

마냥 어렵다고 생각하고 미루고 미루다보니, 마지막 선택과제를 손도 대지 못하게 되었다..
주말이든 평일의 자투리 시간에 테스트코드에 대해 학습하고 테스트 커버리지 작성까지 하고자 다짐하는 시간이었다.


✅ Try

다음 프로젝트에서 시도해볼 점들을 작성했다.

  • 테스트 코드에 대해 학습하고, 테스트 커버리지 활용해보자.
  • 정답을 찾으려고 하지말고 논리적으로 접근해보자.
  • 공부할게 산더미라서 천천히, 차근차근 해보자.
  • 내 코드의 구조와 동작원리를 명확히 이해하고, 설명하기

인프런 회고 문화

profile
wannabe---ing

2개의 댓글

comment-user-thumbnail
2025년 4월 21일

어..어어 SpringContextHolder부터 뇌정지가 와버렸습니다... 나중에 다시 오겠습니다..

1개의 답글