최원종의 개발 블로그

스프링부트 V7 댓글 기능 추가 (User 도메인 리팩토링 ) 본문

Spring boot 입문

스프링부트 V7 댓글 기능 추가 (User 도메인 리팩토링 )

chl6698 2026. 5. 20. 17:19

UserService 리팩토링 진행 부분 정리

메서드 명명 규칙의 변화 : 기존의 영문 명칭(join, login)을
웹 화면의 역할과 일치하는 명확한 한글 네이밍(회원가입, 로그인)으로 변경

회원정보수정화면 분리 : 단순히 findById를 호출하던 것에서 
'회원정보수정화면'이라는 비즈니스적 목적을 명확히 한 메서드로 개편

수정 로직 내부 의존성 제거 : 회원정보수정 내에서 내부 findById() 메서드를 호출하던 방식을 지우고,
repository.findById()를 직접 호출하여 단독 영속화 수행

로그 메시지 정돈 : 로직 사이에 불필요하게 흩어져 있던 완료 처리 로그를 정리하여
비즈니스 로직 본연의 가독성 향상

 

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 회원가입(UserRequest.JoinDTO joinDTO) {
        log.info("회원가입 서비스 시작");

        userRepository.findByUsername(joinDTO.getUsername()).ifPresent(user -> {
            log.warn("회원가입 실패 - 중복된 사용자명 : {}", user.getUsername());
            throw new Exception400("이미 존재하는 사용자 이름입니다");
        });
        User user = joinDTO.toEntity();

        return userRepository.save(user);
    }

    /**
     * 로그인 처리
     * @param loginDTO (사용자가 요청한 로그인 정보)
     * @return User(조회된 정보 세션 저장용)
     */
    public User 로그인(UserRequest.LoginDTO loginDTO) {
        log.info("로그인 서비스 시작");
        User userEntity = userRepository.findByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword())
                .orElseThrow(() -> {
                    log.warn("로그인 실패 - 사용자 이름 또는 사용자 비번 잘못 입력");
                    return new Exception400("사용자명 또는 비밀번호가 올바르지 않습니다");
                });

        return userEntity;
    }

    /**
     * 사용자 정보 조회 (프로필 정보 보기 활용)
     * @param id (User PK)
     * @return UserEntity
     */
    public User 회원정보수정화면(Integer id) {
        log.info("사용자 정보 서비스 시작");
        User userEntity = userRepository.findById(id).orElseThrow(() -> {
            log.warn("사용자 정보 조회 실패");
            return new Exception404("사용자 정보를 찾을 수 없습니다");
        });
        return userEntity;
    }


    /**
     * 사용자 정보 수정 처리 (프로필 업데이트)
     * @param id  (User PK)
     * @param updateDTO (사용자가 요청한 데이터)
     * @return User
     */
    @Transactional
    public User 회원정보수정(Integer id, UserRequest.UpdateDTO updateDTO) {
        log.info("회원정보 서비스 시작");
        User userEntity = userRepository.findById(id).orElseThrow(
                () -> new Exception404("사용자 정보를 찾을 수 없습니다"));
        // 더티 체킹 활용
        userEntity.update(updateDTO);
        return userEntity;
    }
}

UserController 리팩토링 정리

서비스 레이어 네이밍 동기화 : UserService의 한글 메서드 개편(회원가입, 로그인 등)에 맞춰 컨트롤러의 호출부를 완벽하게 매핑

변수명 가독성 및 일관성 향상 : 흩어져 있던 userEntity, user 등의 반환 변수명을 
도메인 맥락에 맞게 'updateUser', 'user' 등으로 명확히 정돈

LoginDTO 파라미터명 명확화 : loginProc의 인자명을 loginDTO에서 'reqLoginDTO'로 변경하여
클라이언트로부터 들어온 '요청(Request)' 데이터임을 명시

단일 책임에 집중한 설계 유지 : 비즈니스 언어(용어)가 컨트롤러까지 관통하도록 리팩토링하여
전체 코드의 응집력과 결합도를 이상적으로 유지

 

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 updateUser = userService.회원정보수정(sessionUser.getId(), updateDTO);
        session.setAttribute("sessionUser", updateUser);
        return "redirect:/";
    }

    // 프로필 화면 요청
    @GetMapping("/user/update-form")
    public String updateFormPage(HttpSession session, Model model) {
        User sessionUser = (User) session.getAttribute("sessionUser");
        User user = userService.회원정보수정화면(sessionUser.getId());
        model.addAttribute("user", user);
        return "user/update-form";
    }

    // 로그인 화면 요청
    // 주소 설계 - http://localhost:8080/login-form
    @GetMapping("/login-form")
    public String loginFormPage() {
        return "user/login-form";
    }

    // 로그인 기능 요청
    @PostMapping("/login")
    public String loginProc(UserRequest.LoginDTO reqLoginDTO, HttpSession session) {
        // 인증 검사 x, 유효성 검사 o
        reqLoginDTO.validate();
        User user = userService.로그인(reqLoginDTO);
        session.setAttribute("sessionUser", user);
        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.회원가입(joinDTO);
        return "redirect:/login-form";
    }

}

LoginInterceptor 요약

HandlerInterceptor 구현 : 스프링 MVC 프레임워크가 제공하는 인터셉터 규격을 상속받아
HTTP 요청을 가로채는 서블릿 필터 기능 구현

preHandle() 시점 컨트롤 : 컨트롤러(Handler)에 요청이 도달하기 전인 'preHandle' 단계에서 
세션을 검사하는 선처리 로직 배치

인증 검증 및 예외 발생 : 세션 내 sessionUser 존재 여부를 판별하여,
비로그인 사용자일 경우 즉시 커스텀 예외(Exception401) 처리

@Component를 통한 싱글톤 IoC 관리 : 인터셉터를 스프링 컨테이너의 빈(Bean)으로 등록하여
메모리 효율성 및 관리 편의성 극대화

 

 

LoginInterceptor 코드⬇️

package com.tenco.blog._core.interceptor;

import com.tenco.blog._core.errors.Exception401;
import com.tenco.blog.user.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component // IoC 대상 - 싱글톤 패턴
public  class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      HttpSession session = request.getSession();
      User sessionUser = (User) session.getAttribute("sessionUser");
      if(sessionUser == null) {
          throw new Exception401("로그인 먼저 해주세요");
      }
        return true;
    }
}

UserResponse 코드(주석처리)⬇️

더보기
package com.tenco.blog.user;

public class UserResponse {

//    @Data
//    public static class JoinDTO {
//        private Integer id;
//        private String username;
//        private String email;
//
//        public JoinDTO(User user) {
//            this.id = user.getId();
//            this.username = user.getUsername();
//            this.email = user.getEmail();
//        }
//    } // end of JoinDTO inner class
//
//    @Data
//    public static class SessionDTO extends User {
//        // password 처럼 민감한 정보는 내려주지 않는다.
//        public SessionDTO(User user) {
//            super.setId(user.getId());
//            super.setUsername(user.getUsername());
//            super.setEmail(user.getEmail());
//        }
//    } // end of LoginDTO inner class

} // end of outer class