JPA 19

[트러블 슈팅] SOS 상세 조회 API의 Row Explosion 문제 해결

모든 연관 데이터를 단일 LEFT JOIN 쿼리로 조회하던 구조에서 OneToMany 관계인 SosImage를 별도 쿼리로 분리하여 Row Explosion 문제를 해결하고 조회 안정성을 확보했습니다.그 과정에서 알게 된 내용을 정리해 보겠습니다문제 상황SOS 상세 조회 API를 구현하면서 아래 모든 데이터를 단일 QueryDSL 쿼리에서 LEFT JOIN으로 한 번에 조회하도록 설계했습니다.Member 정보 (badge)ProfileImageBusiness 정보 (name, address)BusinessCodeSos 정보SosImage 리스트문제는 SosImage가 OneToMany 관계라는 점이었습니다. 이미지가 5개라면 결과 Row도 5개가 생성됩니다. 데이터는 하나인데 이미지 수만큼 행이 복제되는..

[트러블슈팅] getReferenceById를 이용한 연관 관계 저장 최적화

문제 상황특정 엔티티를 다른 엔티티의 외래 키(FK)로 저장할 때, 기존에는 findById를 사용하여 해당 엔티티를 DB에서 완전히 조회한 후 세팅하는 방식을 사용했습니다.를 사용하여 해당 엔티티를 데이터베이스에서 완전히 조회한 후 세팅했습니다. 하지만 단순히 연관 관계를 맺기 위해 전체 데이터를 SELECT 하는 것이 비효율적이라는 판단하에 리팩토링을 진행했습니다. java@Transactionalpublic void addFavorite(Long announceId, Long memberId) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new EntityNotFoundException()); ..

[트러블 슈팅]공고 상세 조회 API의 불필요한 DB 쿼리 줄이기

문제 상황공고 상세 조회 API에서 공고 상세 정보(Announce), 사용자의 즐겨찾기 여부(MemberFavorite), 제출 서류 목록(Document) 세 가지 데이터를 함께 반환해야 했습니다.초기 구현에서는 이 세 가지를 모두 개별 쿼리로 분리하여 조회했습니다. sqlSELECT * FROM announce WHERE announce_id = ?SELECT EXISTS ( SELECT 1 FROM member_favorite WHERE member_id = ? AND announce_id = ?)SELECT * FROM document WHERE announce_id = ?API 요청 한 건당 DB와 3번의 Round Trip이 발생하는 구조였습니다. 참고)사실 exist를 쓰는 것..

[트러블슈팅] @OneToOne(mappedBy) LAZY 로딩이 동작하지 않는 이유와 해결 방법

문제 상황회원가입 및 로그인 기능을 구현하던 중 memberRepository.findMemberByEmail() 메서드를 호출했을 때, 조회하지 않은 연관 엔티티의 쿼리까지 함께 실행되는 문제가 발생했습니다.단순히 Member만 조회했지만 실제로는 다음과 같은 SQL이 실행되었습니다. sqlSELECT * FROM member WHERE email = ?SELECT * FROM business WHERE member_id = ?SELECT * FROM profile_image WHERE member_id = ?SELECT * FROM auth WHERE member_id = ? @OneToOne(fetch = FetchType.LAZY)로 설정했기 때문에 연관 엔티티는 실제 접근 시점에 Lazy Lo..

[트러블슈팅] JPA Cascade와 Aggregate Root 적용하기

회원가입 기능을 구현하면서 여러 엔티티가 함께 생성되는 구조를 설계하게 되었습니다.처음에는 각 엔티티를 개별 Repository를 통해 저장하는 방식으로 구현했지만 이 방식은 JPA를 사용하면서도 ORM의 장점을 제대로 활용하지 못하는 구조였습니다.이번 글에서는 회원가입 로직을 Cascade와 Aggregate Root 개념을 활용하여 개선한 과정을 정리해보려고 합니다. 문제 상황회원가입 시 다음과 같은 여러 엔티티가 함께 생성되었습니다.MemberProfileImageAuthBusinessBusinessCode 기존 구현에서는 이렇게 필요한 각 엔티티를 Repository를 통해 각각 개별적으로 저장하였습니다.코드를 보면 아래와 같습니다 기존 회원가입 로직@Transactionalpublic Long..

@JoinColumn의 이해

@JoinColumn 이 JPA를 공부하면서 가장 많이 쓰이는데 초반에 공부를 할 때 대충 외우는 식으로 넘어갔다..나중에 가니까 내가 생각한 방식으로 동작하지 않는 코드를 보면서 JoinColumn에 대해서 찾아봤고 학습한 내용을 정리해 보려고 합니다..! DB의 Join보통 DB에서 Join을 하려면 외래키를 활용해 JOIN을 한다. 예시를 통해 알아보자 여러 명의 Member가 하나의 Team에 가입할 수 있다고 해보면 다음과 같이 Table을 설계하는 게 자연스러운 일이다 membermember_id(pk)member_nameteam_id(fk)1MemberA12MemberB13MemberC1 teamteam_id(pk)team_name1팀A 이렇듯 DB에서는 1:N 관계에서 무조건 N 쪽에..

🌿 스프링/JPA 2026.02.24

API 서버 - 응답 통일 어노테이션 만들기 ( @ApiSuccess )

개요사이드 프로젝트를 하기 전의 우리들만의 아키텍처 규칙을 만들고 싶었다 특히 김영한 님의 스프링 강의를 보면서 MessageSource와 나만의 Annotation을 만들어서 최대한 개발하는데 편의성을 높이고 싶었다 그래서 이번에는 API 응답의 통일성을 가져가고 이를 하면서 느낀 트러블 슈팅 과정을 기록해 볼려고 합니다목표1. 성공 응답과 실패 응답을 우리들만의 Spec 을 만들어서 프런트 엔드 분에게 편의성을 제공하자 예시) 성공응답 { "success": true, "code": 200, "message": "요청에 성공했습니다.", "data": { "id": 1, "name": "dongwook" }} 예시2 ) data 가 없는 성공 응답 { "success": true..

API 예외처리

개요HTML 은 BasicErrorController를 통해서 오류 페이지를 뿌려 주면 되지만 JSON 같은 API는 하나의 오류페이지로 퉁 칠순 없다 각 상황에 맞는 오류 응답 스펙을 정하고 JSON으로 데이터를 내려 줘야 한다 만일 API 컨트롤러에서 오류가 발생하면 JSON 형식으로 응답이 나가야 하는데... 이런 형식으로 응답이 나가면 안된다 이렇게 응답이 나가는 이유는 BasicController 에 응답이 도착할 때 Client의 Accept Header 에따라서 BasicErrorController 가 작동하는 방식이 달라지기 때문이다스프링은 이러한 특정 AcceptHeader (여기서는 application/json) 을 조건으로 RequestMapping 하는 기능도 제공한다 Bas..

서블릿 필터와 스블릿 인터셉터

개요로그인이 안 된 사용자는 로그인하라고 가게하고 로그인이 된 사용자만 우리 페이지를 보여주고 싶다이런 건 모든 컨트롤러의 공통으로 해야 하는 과제다 컨트롤러 하나 하나 하는게 아니라 요청이 오면 요청을 검사하는 공통으로 해야 하는 과제가 생겼다 요구사항 지금 이 사용자가 로그인 됐는지 확인로그인이 안됐으면 리다이렉트 페이지로로그인이 됐으면 계속 이용하게 하자 스프링 필터 필터의 위치HTTP 요청 -> WAS(response,request객체) -> 필터 -> Dispatcher서블릿 -> 컨트롤러 스프링의 필터의 위치는 ServletContainer 안에 존재한다. 그리고 그중에서도 DispatcherServlet 앞단에 존재한다 그래서 필터를 이용해 모든 고객의 요청 로그를 남길 수도 있다 참고..

로그인 처리- 쿠키,세션

개요V1 쿠키 직접 사용V2 커스텀 세션 매니저V3 서블릿 HttpSessionV4 RedirectURL 처리V5 @SessionAttributeV6 ArgumentResolverV7 Filter 인증 처리V8 Interceptor 인증 처리 (최종) V1 - 쿠키에 MemberId 저장@PostMapping("/login")public String login(@Valid @ModelAttribute LoginForm form , BindingResult bindingResult, HttpServletResponse response) { //로그인 성공 처리 Cookie cookie = new Cookie("memberId", String.valueOf((loginMember..