Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
Tags
- IntelliJ IDEA
- Thread
- continue문
- this예약어
- JAVA객체지향
- 인텔리제이 한글 깨짐 해결법
- 상수
- 집합관계
- for문
- multi-threading
- 접근제어지시자
- 컴파일
- 반복문
- 자바 멀티스레딩
- 메서드 오버로딩
- OPP개념
- JAVA기초
- 인텔리제이 기초 설정
- 형 변환
- java변수
- 시스템 환경 변수 편집
- function
- 생성자
- Java
- 메서드
- While
- Java데이터 타입
- break문
- 연관관계
- 포함관계
Archives
- Today
- Total
최원종의 개발 블로그
V5-2 서비스 계층 만들어 보기 (BoardService, UserService) + 리팩토링 본문
Service 계층

Service 계층은 Spring MVC 아키텍처에서 비즈니스 로직을 담당하는 계층이다.
Controller와 Repository 사이에 위치하여 실제 업무 처리를 담당함
BoardService 간단 요약
클래스 레벨 @Transactional(readOnly = true) 도입 : 전체 메서드를 읽기 전용으로 설정하여
JPA 조회 성능 최적화 및 안정성 확보
쓰기 작업 메서드에 @Transactional 분리 : save, updateById, deleteById에만
애노테이션을 따로 붙여 쓰기 권한 부여
Fetch Join 리포지토리 메서드 적용 : findAll()과 findById()에서 FETCH JOIN 쿼리를 호출하여
N+1 성능 문제 완벽 방어
람다 표현식 기반 예외 처리 : Optional의 orElseThrow() 내부에 람다식(() ->)을 적용해
가독성 높은 404 예외 처리 구현
캡슐화 기반 인가(권한) 체크 도입 : boardEntity.isOwner()를 통해 게시글 작성자와 로그인 사용자가
같은지 검증하는 방어 코드 추가
더티 체킹을 통한 데이터 수정 : updateById에서 별도의 update 쿼리 없이
객체의 값만 변경(boardEntity.update)하여 수정 완료
BoardService 핵심 리팩토링 및 변경 포인트 요약
1. 트랜잭션 전략의 이원화 (성능 최적화)
기존: 메서드마다 트랜잭션을 생각 없이 걸거나 빠뜨릴 수 있었음.
변경: 클래스 상단에 @Transactional(readOnly = true)를 기본으로 깔아두어
findAll, findById 같은 조회 메서드들의 성능을 극대화함. (JPA가 스냅샷을 찍지 않아 메모리가 절약됨.)
반면, 데이터 변경이 일어나는 save, updateById, deleteById 메서드 위에만
따로 @Transactional을 붙여서 쓰기 권한을 명확히 분리.
2. 엔티티 객체에 책임을 위임한 인가(Authorization) 처리
Board boardEntity = findById(id);
boardEntity.isOwner(sessionUser.getId()); // 객체지향적 권한 체크
의미: 게시글 수정이나 삭제 시, "이 글을 지울 권한이 있는 사람인가?"를 검증하는 단계.
리팩토링 포인트: 서비스 레이어에서 if문으로 ID를 일일이 비교하는 대신,
Board 엔티티 객체 내부에 isOwner() 메서드를 만들어서 검증을 위임함.
객체지향 설계의 '단일 책임 원칙(SRP)'을 적용한 코드.
3. 지난번에 학습한 내용들의 완벽한 조화
Optional + 람다식: findById에서 글이 없을 때
예외를 던지는 코드가 orElseThrow(() -> new Exception404(...)) 형태로 깔끔하게 구현함.
더티 체킹: updateById 메서드에서 리포지토리의 save나 수정 메서드를 호출하지 않고,
영속화된 엔티티의 데이터만 바꿈으로써(boardEntity.update(updateDTO)) 수정함.
N+1 방어: 이전에 BoardRepository에 직접 만들었던 findAllJoinUser()와 findByIdJoinUser()를
서비스에서 호출하여 작성자 정보까지 한방 쿼리로 긁어오도록 연결함.
BoardService 코드 ⬇️
더보기
package com.tenco.blog.board;
import com.tenco.blog._core.errors.Exception404;
import com.tenco.blog.user.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
// 객체지향 개념 - 단일 책임에 원칙
@Slf4j
// 서비스 계층은 @Service 어노테이션으로 IoC 처리
@Service // IoC 처리
@RequiredArgsConstructor // DI 처리
@Transactional(readOnly = true)
// 모든 메서드를 읽기 전용 트랜잭션으로 실행(findAll, findById 등 조회에 적합)
// 성능 최적화(변경 감지 비활성화 됨), 즉 조회시 데이터 수정 방지
public class BoardService {
// @Autowired
private final BoardRepository boardRepository;
// 게시글 저장
// 데이터 수정이 필요하므로 깊은 트랜잭션 처리
// (읽기 전용 트랜잭선을 해제, 쓰지 전용 트랜잭션으로 변경)
@Transactional
public Board save(BoardRequest.SaveDTO saveDTO, User sessionUser) {
// 1. 로그 기록 - 게시글 저장 요청 정보
// 2. DTO를 Entity로 변환(작성자 정보 포함)
// 3. 데이터베이스에 게시글 저장
// 4. 저장 완료 로그 기록
// 5. 저장된 게시글을 컨트롤러 단으로 반환
log.info("게시글 저장 서비스 시작 - 제목 : {}, 작성자 : {}",
saveDTO.getTitle(), sessionUser.getUsername());
Board board = saveDTO.toEntity(sessionUser);
Board savedBoardEntity = boardRepository.save(board);
log.info("게시글 저장 완료 - ID : {}, 제목 : {}",
savedBoardEntity.getId(), savedBoardEntity.getTitle());
return savedBoardEntity;
}
// 게시글 목록 조회
public List<Board> findAll() {
// 1. 로그 기록 - 게시글 목록 조회
// 2. 데이터베이스 접근해서 모든 게시글 목록을 조회
// 3. 로그 기록 - (총 개시글 수)
// 4. 조회된 게시글 목록을 컨트롤러로 반환
log.info("게시글 목록 조회 서비스");
List<Board> boardList = boardRepository.findAllJoinUser();
log.info("게시글 목록 조회 완료 - 총 : {}", boardList.size());
return boardList;
}
// 게시글 상세 보기
public Board findById(Integer id) {
// 1. 로그 기록 - 게시글 상세 조회 (id)
// 2. 데이터베이스 접근해서 해당 ID의 게시글 조회 (작성자 정보 포함)
// 3. 게시글이 존재하지 않으면 Exception404로 예외 발생
// 4. 조회 성공시 로그 기록 (제목, 작성자 정보)
// 5. 조회된 게시글 컨트롤러 단으로 반환
log.info("게시글 상세 조회 서비스");
Board boardEntity = boardRepository.findByIdJoinUser(id).orElseThrow(() -> {
log.warn("게시글 조회 실패 - ID: {}", id);
return new Exception404("해당하는 게시글을 찾을 수 없습니다");
}) ;
log.info("게시글 조회 완료 - 제목: {}, 작성자: {}",
boardEntity.getTitle(), boardEntity.getUser().getUsername());
return boardEntity;
}
// 게시글 수정
@Transactional
public Board updateById(Integer id, BoardRequest.UpdateDTO updateDTO, User sessionUser) {
// 1. 로그 기록 - 게시글 수정 요청 정보 (board pk, 새 제목, 요청자)
// 2. 수정하고자 하는 게시글 조회 (중간 삭제 되는 경우도 있음)
// 3. 권한 확인 (인가 처리)
// 4. 권한이 없다면 Exception403 예외 발생
// 5. 더티 체킹으로 게시글 수정 (JPA 영속성 컨텍스트 활용)
// 6. 수정 완료 로그 기록
// 7. 수정된 게시글 반환
log.info("게시글 수정 서비스");
Board boardEntity = findById(id);
boardEntity.isOwner(sessionUser.getId());
// 영속화 되어 있었던 객체의 title, content 의 내용이 변경 됨.
boardEntity.update(updateDTO);
log.info("게시글 수정 완료 - ID : {}, 새 제목: {}",
boardEntity.getId(), boardEntity.getTitle());
return boardEntity;
}
// 게시글 삭제 (권한 체크 포함)
@Transactional
public void deleteById(Integer id, User sessionUser) {
// 1. 로그 기록 - 게시글 삭제 요청 정보 (board PK, 요청자)
// 2. 삭제하려는 게시글 조회
// 3. 권한 확인 - 게시글 작성자와 요청자가 동일한지 확인
// 4. 권한이 없다면 Exception403 예외 발생
// 5. 데이터베이스에서 게시글 삭제 실행
// 6. 삭제 완료 로그 기록
log.info("게시글 삭제 서비스");
Board boardEntity = findById(id);
boardEntity.isOwner(sessionUser.getId());
boardRepository.deleteById(id);
log.info("게시글 삭제 완료 - ID : {}", id);
}
/**
* 게시글 수정 화면 요청(인가 처리)
* @param id (Board PK)
* @param sessionUser (로그인한 사용자 정보)
* @return Board
*/
public Board findByIdAndCheckOwner(Integer id, User sessionUser) {
log.info("게시글 수정 화면 조회 서비스");
Board boardEntity = findById(id);
boardEntity.isOwner(sessionUser.getId());
log.info("게시글 수정 조회 완료 - 제목: {}, 작성자: {}",
boardEntity.getTitle(), boardEntity.getUser().getUsername());
return boardEntity;
}
}
Board 코드 요약
@Entity 및 @Table 설정 : Board 클래스를 데이터베이스의 board_tb 테이블과 1:1로 매핑하여
JPA 관리 대상으로 등록
기본키(PK) 전략 변경 : GenerationType.IDENTITY를 사용하여
ID 생성을 MySQL 등 DB의 AUTO_INCREMENT 기능에 위임
연관관계 매핑 (ManyToOne) : 기존의 평문 username 필드를 삭제하고,
User 엔티티와 다대일(@ManyToOne) 외래키(user_id) 관계로 연결
지연 로딩(FetchType.LAZY) 선택 : Board 조회 시 무조건 User를 조인하지 않고,
실제 필요할 때만 추가 조회하여 성능 최적화 기반 마련
@CreationTimestamp 자동화 : 하이버네이트 기능을 활용해 글이 저장되는 시점의 서버 시간을
createdAt 컬럼에 자동 주입
객체지향적 편의 메서드 구현 : 엔티티 내부에서 스스로 데이터를 수정(update)하고
소유권을 검증(isOwner)하는 방어 로직 완성
Board 핵심 매핑 및 변경 포인트 상세 분석
1. 관계형 DB 구조를 반영한 객체 지향적 연관관계 매핑
// private String username; <- 삭제
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
리팩토링 포인트: 기존 방식처럼 테이블에 단순히 작성자 이름(username) 문자열만 박아두면,
작성자의 이메일이나 프로필 사진을 가져오기 위해 또 우회해서 쿼리를 날려야 했음.
개선 효과: 문자열 필드를 지우고 User 엔티티 통째로 관계를 맺어줌으로써,
객체 세상에서는 board.getUser().getEmail()처럼 자유롭게
작성자의 모든 정보에 패스(Graph Navigate)할 수 있게 됨.
2. 성능 최적화의 대원칙: FetchType.LAZY (지연 로딩)
EAGER (즉시 로딩): 게시글 1건만 조회해도 무조건 작성자 정보까지 JOIN해서 가져옴.
당장 작성자 이름이 필요 없는 대량의 통계 조회 등에서도 무조건 조인이 걸려 성능 저하의 주범이 됨.
LAZY (지연 로딩): boardJPARepository.findById()를 할 때는 딱 board_tb만 조회.
연관된 User는 나중에 board.getUser().getUsername()처럼 실제로 닉네임을 꺼내 쓰려고 할 때
카카오톡 알림 오듯 DB에 쿼리를 싹 날려 채워 넣음.
팁: 실무에서는 무조건 LAZY를 기본으로 깔고,
한방 쿼리가 필요할 때만 앞서 배운 JOIN FETCH를 명시하는 것이 정석.
3. 단일 책임 원칙(SRP)을 지킨 객체지향 편의 메서드
update(updateDTO): 엔티티 본인이 직접 상태를 변경하게 함으로써
데이터 오염을 막고 안전하게 더티 체킹을 수행.
isOwner(sessionUserId): 서비스 레이어에서 하던 권한 체크(인가) 책임을 데이터 주인인 Board 엔티티에게 넘김.
조건에 맞지 않으면 본인이 직접 Exception403을 터트림
Board 코드 ⬇️
더보기
package com.tenco.blog.board;
import com.tenco.blog._core.errors.Exception403;
import com.tenco.blog.user.User;
import com.tenco.blog.util.MyDateUtil;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.sql.Timestamp;
@Data // get,set, toString ..
// @Entity - JPA가 이 클래스를 데이터베이스 테이블과 매핑하는 객체로 인식하게 설정
// 즉, 이 어노테이션이 있어야 JPA가 관리 함
@Entity
@Table(name = "board_tb")
@NoArgsConstructor
@AllArgsConstructor // 전체 멤벼 번수를 넣을 수 있는 생성자.
@Builder
public class Board {
// @id : 이 필드가 기본키임을 설정 함
@Id
// IDENTITY 전략: 데이터베이스게 기본 AUTO_INCREMENT 기능 사용
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
// private String username; 삭제해야 함.
private String title;
private String content;
// 연관관계 설정 해주어야 한다.
// 다대일 연관관계 : 여러개 게시글이 하나의 사용자에게 속한다.
// FetchType 전략 : EAGER, LAZY
// EAGER - 조회시 한번에 다 들고 와라 ( 1번 게시글 조회시 한번 조인까지 해라)
// LAZY - 처음부터 Board 조회할 때 User 정보를 가져오지 마. 필요할 때 한번 더 조회 해.
@ManyToOne(fetch = FetchType.LAZY)
// @OneToMany
// @OneToOne
@JoinColumn(name = "user_id") // 외래키 컬럼명 표시 됨
private User user;
// @CreationTimestamp : 하이버네이트가 제공하는 어노테이션
// 특정 하나의 엔티티가 저장이 될 때 현재 시간을 자동으로 저장해 설정
// now() 명시할 필요 없음
// pc --> db (자동 날짜 주입)
@CreationTimestamp
private Timestamp createdAt;
// createdAt -> 포멧 하는 메서드 만들어 보기
public String getTime() {
return MyDateUtil.timestampFormat(createdAt);
}
// 수정 편의 기능 만들기
public void update(BoardRequest.UpdateDTO updateDTO) {
// this.username = updateDTO.getUsername(); 삭제 예정
this.title = updateDTO.getTitle();
this.content = updateDTO.getContent();
// 더티체킹 - 변경 감지 동작 과정
// 1. 최초 조회시 영속성 컨텍스트 1차 캐쉬에 데이터를 스냅샷으로 보관 함.
// 2. 영속화된 엔티티가(board)의 멤버 변수값이 변경이 된다면
// 1차에서 보관했던 값과 2차에서 수정된 필드값을 비교 함.
// 3. 변화가 발생이 되었다면 트랜잭션 커밋 시점에 변경된 필드값 UPDATE 쿼리 자동 생성
// 4. 물리적은 DB에 반영 됨.
}
// 편의 기능 - 게시글 소유자 확인을 위한 기능 추가
public boolean isOwner(Integer sessionUserId) {
if(!this.user.getId().equals(sessionUserId)) {
throw new Exception403("본인이 작성한 게시글이 아닙니다");
}
return true;
}
//
}
BoardController 코드 요약
@Controller 기반의 SSR 처리 : JSON 데이터 대신 Mustache 템플릿(뷰) 경로 문자열을 반환하여
서버 사이드 렌더링 구현
Model 객체를 통한 데이터 전달 : 조회한 게시글 데이터(boardList, board)를
model.addAttribute()에 담아 뷰 화면으로 전달
PathVariable 기능 활용 : /board/{id} 처럼 URL 경로에 포함된 가변적인 게시글 고유 ID 값을
안전하게 파싱하여 로직에 활용
DTO 자체 검증(validate()) 수행 : 글 저장 및 수정 프로세스 진행 직전,
DTO 내부의 입력값 유효성 검사를 호출하여 1차 방어선 구축
흐름 제어(redirect)의 정석 활용 : 저장, 수정, 삭제 같은 데이터 변경(POST) 요청 처리 완료 후
"redirect:/"를 통해 화면 새로고침(F5) 중복 요청 방지
BoardController 리팩토링 코드 ⬇️
더보기
package com.tenco.blog.board;
import com.tenco.blog.user.User;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
@Slf4j
@Controller // IoC
@RequiredArgsConstructor // DI
public class BoardController {
private final BoardService boardService;
/**
* 게시글 작성 화면 요청
* @return 페이지 반환
* 주소설계 : http://localhost:8080/board/save-form
*/
@GetMapping("/board/save-form")
public String saveForm(HttpSession httpSession) {
return "board/save-form";
}
/**
* 게시글 작성 기능 요청
* @return 페이지 반환
* 주소설계 : http://localhost:8080/board/save-form
*/
@PostMapping("/board/save")
public String saveProc(BoardRequest.SaveDTO saveDTO, HttpSession session) {
User sessionUser = (User) session.getAttribute("sessionUser");
saveDTO.validate();
boardService.save(saveDTO, sessionUser);
return "redirect:/";
}
/**
* 게시글 목록 화면 요청
* 주소설계 : http://localhost:8080/
*/
@GetMapping({"/", "index"})
public String list(Model model) {
List<Board> boardList = boardService.findAll();
model.addAttribute("boardList", boardList);
return "board/list";
}
// 게시글 상세보기 화면 요청
// http://localhost:8080/board/1
@GetMapping("/board/{id}")
public String detailPage(@PathVariable(name = "id") Integer id, Model model) {
Board board = boardService.findById(id);
model.addAttribute("board", board);
return "board/detail";
}
// 삭제 기능 요청
@PostMapping("/board/{id}/delete")
public String deleteProc(@PathVariable(name = "id") Integer id, HttpSession session) {
User sessionUser = (User) session.getAttribute("sessionUser");
boardService.deleteById(id, sessionUser);
return "redirect:/";
}
// http://localhost:8080/board/1/update-form
// 게시글 수정 화면 요청
@GetMapping("/board/{id}/update-form")
public String updateFormPage(@PathVariable(name = "id") Integer id, Model model, HttpSession session) {
User sessionUser = (User) session.getAttribute("sessionUser");
Board boardEntity = boardService.findByIdAndCheckOwner(id, sessionUser);
model.addAttribute("board", boardEntity);
return "board/update-form";
}
@PostMapping("/board/{id}/update")
public String updateProc(@PathVariable(name = "id") Integer id,
BoardRequest.UpdateDTO updateDTO, HttpSession session) {
User sessionUser = (User) session.getAttribute("sessionUser");
updateDTO.validate();
boardService.updateById(id, updateDTO, sessionUser);
return "redirect:/board/" + id;
}
}
UserService 코드 요약
클래스 레벨 트랜잭션 최적화 : @Transactional(readOnly = true)를 기본으로 설정하여
조회 성능 최적화 및 영속성 오버헤드 최소화
ifPresent() 활용 중복 검사 : findByUsername() 결과가 존재할 경우(ifPresent)
즉시 Exception400을 터트리는 세련된 중복 방지 로직 구현
orElseThrow() 람다식 예외 처리 : 로그인 및 단건 조회 시 데이터가 없으면
각각 상황에 맞는 커스텀 예외(400, 404)를 람다식으로 던지도록 설계
더티 체킹 기반 프로필 수정 : updateById() 수행 시 별도 쿼리 없이 영속화된 userEntity.update()를 통해
트랜잭션 종료 시점에 자동 반영
세션 동기화를 고려한 반환 설계 : 수정된 유저 객체를 컨트롤러로 다시 반환하여,
호출부에서 세션(Session) 정보를 최신화할 수 있도록 배려
UserService 코드
더보기
package com.tenco.blog.user;
import com.tenco.blog._core.errors.Exception400;
import com.tenco.blog._core.errors.Exception404;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* User 관련 비즈니스 로직을 처리하는 Service 계층
* Controller 와 Repository 사이에서 실제 업무 로직을 담당
*/
@Slf4j
@Service // IoC
@RequiredArgsConstructor // DI
@Transactional(readOnly = true) // 기본적인 읽기 전용 트랜잭션 처리 , 조회시 더티 체킹 안 일어남
public class UserService {
private final UserRepository userRepository;
/**
* 회원 가입 처리
* @param joinDTO (사용자 회원가입 요청 정보)
* @return User (저장된 사용자 정보)
*/
@Transactional
public User join(UserRequest.JoinDTO joinDTO) {
// 1. 로그 기록 - 회원가입 요청 정보
// 2. 사용자명 중복 검사 (데이베이스 조회)
// 3. username 존재하면 Exception400 예외 발생
// 4. JoinDTO -> User 객체로 변환 처리
// 5. 데이터 베이스에 사용자 정보 저장
// 6. 로그 기록 - 회원가입 완료
// 7. 저장된 사용자 정보 컨트롤러로 반환
log.info("회원가입 서비스 시작");
// 조건 - 중복된 사용자이름이 없는것이 정상 동작
// ifPresent -> 존재 여부 확인
// 사용자명 중복 체크
userRepository.findByUsername(joinDTO.getUsername()).ifPresent(user -> {
log.warn("회원가입 실패 - 중복된 사용자명 : {}", user.getUsername());
throw new Exception400("이미 존재하는 사용명입니다");
});
User user = joinDTO.toEntity();
User savedUserEntity = userRepository.save(user);
log.info("회원 가입 서비스 완료 - id : {}", savedUserEntity.getId());
return savedUserEntity;
}
/**
* 로그인 처리
* @param loginDTO (사용자가 요청한 로그인 정보)
* @return User(조회된 정보 세션 저장용)
*/
public User login(UserRequest.LoginDTO loginDTO) {
// 1. 로그 기록 - 로그인 요청 정보 (사용자명)
// 2. 사용자 이름과 비밀번호로 데이터베이스에서 조회
// 3. 인증 정보기 일치하지 않으면 Exception400 예외 처리
// 4. 로그 기록 - 로그인 성공 정보
// 5. 인증된 사용자 정보 컨트롤러 단으로 반환 (세션 저장용)
log.info("로그인 서비스 시작");
User userEntity = userRepository.findByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword())
.orElseThrow(() -> {
log.warn("로그인 실패 - 사용자 이름 또는 사용자 비번 잘못 입력");
return new Exception400("사용자명 또는 비밀번호가 올바르지 않습니다");
});
log.info("로그인 성공 - 사용자명 : {} ", loginDTO.getUsername());
return userEntity;
}
/**
* 사용자 정보 조회 (프로필 정보 보기 활용)
* @param id (User PK)
* @return UserEntity
*/
public User findById(Integer id) {
log.info("사용자 정보 서비스 시작");
return userRepository.findById(id).orElseThrow(() -> {
log.warn("사용자 정보 조회 실패");
return new Exception404("사용자 정보를 찾을 수 없습니다");
});
}
/**
* 사용자 정보 수정 처리 (프로필 업데이트)
* @param id (User PK)
* @param updateDTO (사용자가 요청한 데이터)
* @return User
*/
@Transactional
public User updateById(Integer id, UserRequest.UpdateDTO updateDTO) {
// 1. 로그 기록 - 회원 정보 수정 요청 정보 (ID)
// 2. 수정하려면 사용자 정보 조회
// 3. 예외 처리 Exception400
// 4. 더티 체킹을 통한 사용자 정보 수정(JPA 영속성 컨텍스트 활용)
// 5. 로그 기록 - 수정 완료 로그 처리
// 6. 수정된 사용자 정보 컨트롤로 단으로 반환 (세션 동기화 용)
log.info("회원정보 서비스 시작");
User userEntity = findById(id);
// 더티 체킹 활용
userEntity.update(updateDTO);
log.info("회원정보 수정 완료 - 사용 ID : {}", userEntity.getId());
return userEntity;
}
}
User 코드 요약
@Entity 및 @Table 설정 : User 클래스를 데이터베이스의 user_tb 테이블과 매핑하여
JPA 영속성 컨텍스트의 관리 대상으로 등록
@Column(unique = true) 적용 : 회원가입 시 아이디 중복을 데이터베이스 레벨에서
원천 차단하기 위해 username 컬럼에 유니크 제약 조건 부여
@Builder 패턴의 생성자 제한 적용 : 클래스 레벨이 아닌 필요한 필드만 모은 커스텀 생성자 위에
@Builder를 붙여 안전한 객체 생성 유도
@CreationTimestamp 자동화 : 회원가입이 완료되어 영속화(persist)되는 시점에
서버의 현재 시간을 createdAt 컬럼에 자동으로 주입
객체지향적 도메인 메서드(update) 구현 : Setter를 배제하고 무분별한 데이터 수정을 막기 위해
엔티티 내부에서 스스로 데이터를 변경하도록 설계
User 코드(수정 기능 추가)
더보기
package com.tenco.blog.user;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.sql.Timestamp;
@Data
@NoArgsConstructor
@Table(name = "user_tb")
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
// 사용자명 중복 방지를 위한 유니크 제약 조건 설정
@Column(unique = true)
private String username;
private String password;
private String email;
// 엔티티가 영속화 될 때 자동으로 현재 시간을 주입해라 pc -> db
@CreationTimestamp
private Timestamp createdAt;
@Builder
public User(Integer id, String username, String password, String email, Timestamp createdAt) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
this.createdAt = createdAt;
}
// 편의 기능 추가 - 회원 정보 수정
public void update(UserRequest.UpdateDTO updateDTO) {
this.password = updateDTO.getPassword();
// Dirty Checking 처리
}
}
UserController 코드 요약
인증 및 가입 흐름 제어 : 로그인/회원가입의 화면 요청(GET)과 기능 요청(POST)을 분리하여
직관적인 라우팅 설계 완수
DTO 유효성 검사 자동화 : loginProc, joinProc, updateProc 진입 직후
validate()를 호출해 올바른 데이터만 서비스로 전달
HttpSession 기반 상태 유지 : 로그인 성공 시 세션에 유저 엔티티를 저장(sessionUser)하고,
로그아웃 시 세션을 무효화(invalidate) 처리
핵심 기술 - 세션 동기화 구현 : 프로필 수정 시, DB 변경 후 리턴받은 최신 객체로
세션 속성을 갱신하여 뷰 화면에 즉시 반영 성공
POST-Redirect-GET 패턴 적용 : 가입, 로그인, 수정 등의 쓰기 작업 완료 후
redirect를 사용해 브라우저 새로고침으로 인한 중복 요청 방지
UserController코드(수정)
더보기
package com.tenco.blog.user;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Slf4j
@Controller // IoC
@RequiredArgsConstructor // DI 처리
public class UserController {
private final UserService userService;
// 프로필 수정 기능 요청
@PostMapping("/user/update")
public String updateProc(UserRequest.UpdateDTO updateDTO, HttpSession session) {
updateDTO.validate();
User sessionUser = (User) session.getAttribute("sessionUser");
User userEntity = userService.updateById(sessionUser.getId(), updateDTO);
// 세션 동기화 처리
session.setAttribute("sessionUser", userEntity);
return "redirect:/";
}
// 프로필 화면 요청
@GetMapping("/user/update-form")
public String updateFormPage(HttpSession session, Model model) {
User sessionUser = (User) session.getAttribute("sessionUser");
User userEntity = userService.findById(sessionUser.getId());
model.addAttribute("user", userEntity);
return "user/update-form";
}
// 로그인 화면 요청
// 주소 설계 - http://localhost:8080/login-form
@GetMapping("/login-form")
public String loginFormPage() {
// 인증 검사 x , 유효성 x
return "user/login-form";
}
// 로그인 기능 요청
@PostMapping("/login")
public String loginProc(UserRequest.LoginDTO loginDTO, HttpSession session) {
// 인증 검사 x, 유효성 검사 o
loginDTO.validate();
User userEntity = userService.login(loginDTO);
session.setAttribute("sessionUser", userEntity);
return "redirect:/";
}
// 로그아웃 기능 요청
@GetMapping("/logout")
public String logout(HttpSession session) {
// 세션 메모리에 내 정보를 없애 버림
session.invalidate();
return "redirect:/";
}
// 회원 가입 화면 요청
// 주소 설계 - http://localhost:8080/join-form
@GetMapping("/join-form")
public String joinFormPage() {
return "user/join-form";
}
// 회원 가입 기능 요청
// 주소 설계 - http://localhost:8080/join
@PostMapping("/join")
public String joinProc(UserRequest.JoinDTO joinDTO) {
// 인증검사 x, 유효성 검사 하기 o
joinDTO.validate();
userService.join(joinDTO);
return "redirect:/login-form";
}
}
'Spring boot 입문' 카테고리의 다른 글
| V6-2 expose-session-attributes 의 false 사용 (0) | 2026.05.20 |
|---|---|
| 스프링부트 V6- OSIV (0) | 2026.05.20 |
| 스프링부트 V5 JPARepository + Service(JpaRepository 인터페스와 Optional 타입) (0) | 2026.05.19 |
| Spring Boot 필수 어노테이션(Annotation) 총정리 (0) | 2026.05.18 |
| V4-3 intercepter 활용 (인증검사 공통 처리) (0) | 2026.05.18 |
