최원종의 개발 블로그

V3-4 회원가입(사용자 등록과 JPA 영속성 활용) 본문

Spring boot 입문

V3-4 회원가입(사용자 등록과 JPA 영속성 활용)

chl6698 2026. 5. 12. 16:02

회원가입화면


UserRepository 코드

 

중요 개념 및 핵심 요약

SRP (Single Responsibility Principle) 개념
단일 책임 원칙(SRP)은 객체지향 설계의 5대 원칙(SOLID) 중 첫 번째 원칙.

정의: 하나의 클래스는 단 하나의 책임(기능)만 가져야 한다.

판단 기준: 클래스를 수정해야 하는 이유는 오직 하나뿐이어야 한다.

목적: 코드의 응집도를 높이고 결합도를 낮추어,
변경 사항이 발생했을 때 다른 코드에 미치는 영향을 최소화하기 위함.
핵심 요약

적용된 책임	Persistence(영속성): 오직 데이터베이스와의 통신 및 데이터 입출력만 담당함.
분리된 책임	비즈니스 로직(Service), 화면 렌더링(Controller), 데이터 전송(DTO) 등은 포함하지 않음.
장점	만약 DB를 MySQL에서 Oracle로 바꾸거나 JPA 설정을 변경할 때, 오직 이 클래스만 수정하면 됨.

package com.tenco.blog.user;

import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

// SRP - 단일 책임의 원칙
@Repository // IoC 대상
@RequiredArgsConstructor
public class UserRepository {

    //DI - 스프링 프레임 워크가 주소값 자동 주입
    private final EntityManager em;

    // 회원 가입 요청시 --> INSERT
    @Transactional
    public User save(User user) {
        // 매개 변수로 들어온 User Object 비영속 상태이다.
        em.persist(user);
        // 리턴시 User Object 는 영속화 된 상태이다.
        return user;
    }

    // 사용자 이름 중복 확인
    public User findByUsername(String username) {
        String jpqlStr = """
                SELECT u FROM User u WHERE u.username = :username
                """;

//        Query query = em.createQuery(jpqlStr, User.class);
//        query.setParameter("username", username);
//        User userEntity = (User) query.getSingleResult();
        try {
            return em.createQuery(jpqlStr, User.class)
                    .setParameter("username", username)
                    .getSingleResult();
        } catch (Exception e) {
            return null;
        }

    }


    // 로그인 요청시 --> SELECT
    public User findByUsernameAndPassword(String username, String password) {
        String jpqlStr = """
                SELECT u FROM User u WHERE u.username = :username AND u.password = :password   
                """;
        try {
            return em.createQuery(jpqlStr, User.class)
                    .setParameter("username", username)
                    .setParameter("password", password)
                    .getSingleResult();
        } catch (Exception e) {
            return null;
        }
    }
}

 


Usercontroller 코드

핵심 메서드 분석

1. 회원가입 페이지 요청(joinFormPage)
방식: GET 요청
역할:단순히 사용자에게 데이터를 입력받을 HTML 폼(user/join-form.mustache)을 보여줌

2. 회원가입 기능 수행 (joinProc)
방식: POST 요청 (데이터 생성 및 보안을 위해 사용)

DTO 활용: UserRequest.JoinDTO를 사용하여
클라이언트가 보낸 데이터를 한 번에 바인딩함. (파싱 전략 2)
로직 흐름

1. 유효성 검사 (validate): 입력값이 비어있거나 형식에 맞지 않는지 확인함.

2. 비즈니스 로직 (중복 검사): userRepository.findByUsername을 통해 
이미 존재하는 아이디인지 체크함. 중복 시 예외를 던짐.

3. 영속화 (save): DTO를 엔티티로 변환(toEntity)하여 DB에 저장함.

4. 응답 (redirect): 성공 시 메인 페이지(/)로 사용자를 보냄.

package com.tenco.blog.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Slf4j
@Controller // IoC
@RequiredArgsConstructor // DI 처리
public class UserController {

    private final UserRepository userRepository;

    // 회원 가입 화면 요청
    // 주소 설계 - http://localhost:8080/join-form
    @GetMapping("/join-form")
    public String joinFormPage() {

        return "user/join-form";
    }

    // 회원 가입 기능 요청
    // 주소 설계 - http://localhost:8080/join
    // 파싱 전략 1 - key=value 구조 (@RequestParam 사용)
    // 파싱 전략 2 - Object DTO 설계
    @PostMapping("/join")
    public String joinProc(UserRequest.JoinDTO joinDTO) {

        log.info("username " + joinDTO.getUsername());
        log.info("password " + joinDTO.getPassword());
        log.info("email " + joinDTO.getEmail());
        // 1. 유효성 검사 하기
        joinDTO.validate(); // 유효성 검사 ---> 오류 --> 예외 처리 넘어감
        // 회원가입 요청 전 ==> 중복 username 검사
        User userCheckName = userRepository.findByUsername(joinDTO.getUsername());

        if(userCheckName != null) {
            throw new IllegalArgumentException("이미 사용중인 username 입니다 : "
                    + userCheckName.getUsername());
        }
        userRepository.save(joinDTO.toEntity());
        // TODO
        // 로그인 화면으로 리다이렉트 처리 예정
        return "redirect:/";
    }

}

UserRequest 코드

핵심 기능 정리

1. 정적 내부 클래스 활용 (static class JoinDTO)
UserRequest라는 큰 바구니 안에 JoinDTO, LoginDTO 등 목적에 맞는 작은 바구니들을 모아서 관리함.
관련 있는 DTO들을 한 클래스에서 관리하여 구조가 깔끔해짐.

2. 엔티티 변환 메서드 (toEntity)
역할: DTO에 담긴 데이터를 바탕으로 실제 DB에 저장될 User 엔티티 객체를 생성.

장점: 컨트롤러에서 복잡하게 new User(...)를 호출할 필요 없이,joinDTO.toEntity() 한 줄로 깔끔하게 엔티티화가능. 
이때 이전에 구현한 빌더 패턴을 활용하여 안전하게 객체를 생성.

3. 자체 유효성 검사 (validate)
로직: null 체크, 공백 체크(isEmpty), 그리고 이메일 형식(@ 포함 여부) 등을 검증.
효과: 잘못된 데이터가 서비스 레이어나 DB까지 흘러 들어가지 않도록 입구(DTO)에서 원천 차단. 
문제가 있을 경우 IllegalArgumentException을 던져 예외 처리를 유도.
package com.tenco.blog.user;

import lombok.Data;

public class UserRequest {

    // 회원가입 DTO
    @Data
    public static class JoinDTO {
        private String username;
        private String password;
        private String email;

        // 편의 기능 추가 - 내가 가지고 있는 멤버 변수에 값으로 User 엔티를 생성
        public User toEntity() {
            return User.builder()
                    .username(username)
                    .password(password)
                    .email(email)
                    .build();
        }

        // 유효성 검사 메서드 만들기
        public void validate() {
            if (username == null || username.trim().isEmpty()) {
                throw new IllegalArgumentException("사용자명은 필수 입니다");
            }

            if(password == null || password.trim().isEmpty()) {
                throw new IllegalArgumentException("비밀번호는 필수 입니다");
            }

            if(email == null || email.trim().isEmpty()) {
                throw new IllegalArgumentException("이메일은 필수 입니다");
            }
            // 입력값 : abc@naver.com --> contains() -->   true   --> ! --> false
            if(email.contains("@") == false) {
                throw new IllegalArgumentException("올바른 이메일 형식이 아닙니다");
            }

        }

    }

}
팁!
DTO를 따로 만드는 이유

Entity (User)
역할 : DB 테이블과 직접 매핑되는 설계도
수정 빈도: DB 구조가 바뀌지 않는 한 거의 없음
민감 정보: 비밀번호, 생성일 등 모든 정보 포함

DTO (JoinDTO)
역할 : 화면(UI)에서 넘어오는 데이터를 받는 바구니
수정 빈도 : 화면 디자인이나 기획이 바뀌면 자주 변경됨
민감 정보 : 필요한 정보(ID, PW, Email)만 선택적으로 포함

join-form.mustache 코드

{{> layout/header}}
<div class="container p-5 flex-grow-1">
    <div class="card">
        <div class="card-header"><b>회원가입을해주세요</b></div>
        <div class="card-body">
            <form action="/join" method="post">
                <div class="mb-3">
                    <input type="text" class="form-control" placeholder="enter username" name="username">
                </div>
                <div class="mb-3">
                    <input type="password" class="form-control" placeholder="enter password" name="password">
                </div>
                <div class="mb-3">
                    <input type="email" class="form-control" placeholder="enter email" name="email">
                </div>
                <button type="submit" class="btn btn-primary form-control">회원가입</button>
            </form>
        </div>
    </div>
</div>
{{> layout/footer}}