@EntityGraph
기반으로 코드 개선현재 프로젝트에서 만족했고, 앞으로의 훈련기간에서 지속하고 싶은 부분을 작성했다.
현재 프로젝트에서 어려웠던 점과 아쉬웠던 점을 작성했다.
==null
사용은 지양하는게 맞지 않나요? Optional
을 쓰면 되잖아요!무의식적으로 ==null
쓰는거 자체를 코드를 작성하는데 있어 피해야 된다고 생각했다.
보통 해당 코드를 간단하게, 급하게 사용해서 그런가..?
null
은 참조형 변수가 가르키는 객체의 주소가 없을 때 발생한다.null
일 경우, Optional로 감싸 반환하여 사용한다.==null
보다 많은 기능들을 제공하고, 그만큼 비용이 든다는 말이다.todo.getUser().getId();
위에 코드는 메서드체이닝(고리처럼 메서드를 연결하여 호출)이라고 하는데,
위와 같은 상황은 NPE에 취약하다.
디버깅이 어렵기 때문에 어디서 null
인지 찾는 시간이 많이 소요된다.
💡 "이걸 어디서 처리해야 될까?" 라는 질문을 해봤나??
❗️ null 체크를 뭘로 해야되요? 가 아니라 서비스 레이어에서 해당 로직이 존재하는지 맞나요? 라는 질문을 받았다.
Todo 엔티티 자체에서 작성자가 있는지 판단해서 아이디를 꺼내주는 메서드를 제공하는 게 더 나은 설계가 아닐까 하는 말씀을 주셨다!
null
이 들어올 수 있는 상황을 애초에 구조적으로 막는 것,
그리고 그 책임을 어디에 둘지 고민하는 것이 정말 중요하다는 걸 느꼈다.
단순히 "== null은 나쁘다", "Optional을 써야 한다"가 아니라
NPE가 발생할 가능성, 디버깅의 난이도, 성능에 주는 영향, 가독성, 책임 위치 등등..
모든걸 따져보고 각 상황에 맞게 선택하는 게 더 현명한 접근이라는 생각이 들었다!
나는 항상 "정답"을 찾으려는 경향이 있었던 것 같다.
근데 이번에 받은 피드백을 통해,
정답보단 "논리적인 근거를 바탕으로 결정하는 것"이 더 중요하다는 걸 느꼈다.
앞으로도 이런 고민을 통해, 내 코드가 더 설득력 있고 견고해졌으면 좋겠다는 생각이다!
✅ Bean Prototype Scope는
스프링에서 기본 스코프는 싱글톤이다. 애플리케이션 컨텍스트당 하나의 인스턴스만 생성되어 공유된다. 반면, 프로토타입 스코프는 매번 요청할 때마다 새로운 인스턴스를 생성한다. 따로 인스턴스 삭제를 책임지지 않기 때문에 GC를 통해 삭제된다.
✅ Bean ProtoType Scope가 사용되는 경우
상태를 가지는 객체: 각 인스턴스가 고유한 상태를 유지해야 하는 경우.
예를 들어, 사용자별 설정이나 세션 관련 데이터를 담는 객체 (장바구니?)매 요청마다 새로운 인스턴스가 필요한 경우: 예를 들어, 폼 요소를 동적으로 생성하거나, 각 요청에 따라 다른 설정이 필요한 경우.
런타임에 파라미터를 전달해야 하는 경우: ApplicationContext.getBean()을 사용하여 런타임에 파라미터를 전달하고 새로운 인스턴스를 생성해야 할 때.
기본값인 싱글톤으로도 대부분 구현이 되기 때문에 프로토타입 스코프는 자주 사용되지는 않는다고 한다. 지금은 스코프의 종류들과 특징들만 알고만 있어도 충분하다는 말씀을 주셨다!
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
를 구현할 수도 있다!
{
"bearerToken": "Bearer ${발급된 토큰}"
}
"content": [],
"page": {
"size": 10,
"number": 0,
"totalElements": 0,
"totalPages": 0
}
응답 형식이 제각각이면 클라이언트에서는 어떤 형태가 올지 매번 체크해야 하고,
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
: 클라이언트 요청 URIdata
: 각 요청별 응답 데이터 (존재하지 않을 수도 있다.)위와 같이 공통 응답 DTO를 만들었고, 아래와 같이 변경되었다.
{
"timestamp": "2025-04-21 09:13:39",
"code": 200,
"status": "OK",
"path": "/auth/signup",
"message": "로그인에 성공하였습니다.",
"data": {
"bearerToken": "Bearer ${발급된 토큰}"
}
}
// 리스트 응답
{
"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
}
}
}
이처럼 응답 구조를 통일하는 건
유지보수와 확장성, 디버깅 편의성까지 고려하면 작은 변화지만 꽤 큰 효과를 주는 리팩토링이었다고 생각한다.
개인적으로 공통으로 설계할 수 있는 부분을 먼저 찾아내고 적용해 나가는 것이
작업의 효율이 올라간다고 생각한다!
마냥 어렵다고 생각하고 미루고 미루다보니, 마지막 선택과제를 손도 대지 못하게 되었다..
주말이든 평일의 자투리 시간에 테스트코드에 대해 학습하고 테스트 커버리지 작성까지 하고자 다짐하는 시간이었다.
다음 프로젝트에서 시도해볼 점들을 작성했다.
어..어어 SpringContextHolder부터 뇌정지가 와버렸습니다... 나중에 다시 오겠습니다..