2026. 4. 29. 21:04ㆍ대우개발원 수업 내용/spring boot, framework
JPA 연동 실무 회원 엔티티 설계 및 중복 방지 회원가입 시스템 구축
1. 값 타입 컬렉션(@ElementCollection)을 활용한 회원 엔티티 설계
- Member 엔티티: 아이디(mid)를 PK로 설정하고, 탈퇴 여부와 소셜 로그인 여부를 포함한 기본 회원 구조를 정의함
- 권한(Role) 관리: MemberRole 열거형(Enum)을 만들고 @ElementCollection을 사용하여 별도의 엔티티 선언 없이 다중 권한(USER, ADMIN)을 관리할 수 있는 값 타입 컬렉션 테이블을 생성함
- 객체 지향적 수정: 외부에서 필드에 직접 접근하지 못하도록 changePassword, addRole 등 전용 메서드를 제공하여 캡슐화를 유지함
2. N+1 문제 해결을 위한 Repository 최적화 및 테스트 데이터 생성
- @EntityGraph 적용: 지연 로딩으로 설정된 권한 정보(roleSet)를 조회 시점에 한꺼번에 가져오도록 attributePaths를 지정하여 데이터베이스 조회 성능을 최적화함
- 데이터 일괄 삽입: PasswordEncoder를 주입받아 비밀번호가 암호화된 100명의 테스트 회원을 생성하고, 조건문(i >= 90)을 통해 특정 사용자에게 ADMIN 권한을 추가 부여하는 검증 과정을 거침
3. 시큐리티 표준 확장 및 MemberSecurityDTO 커스텀 구현
- UserDetails 상속: 시큐리티 내부 인증 객체인 User 클래스를 상속받아 MemberSecurityDTO를 생성하고, 기본 정보 외에 이메일, 탈퇴 여부 등 비즈니스에 필요한 필드를 확장함
- 권한 규격 변환: DB의 권한 데이터를 시큐리티가 인식할 수 있는 SimpleGrantedAuthority로 변환(ROLE_ 접두어 포함)하여 인가 처리가 가능하도록 바인딩함
4. CustomUserDetailsService를 통한 실제 DB 인증 연동
- 인증 프로세스 교체: 기존 하드코딩 방식 대신 MemberRepository를 호출하여 실제 DB의 회원 정보를 조회하는 로직으로 전환함
- 예외 처리: 아이디가 없을 경우 UsernameNotFoundException을 발생시켜 시큐리티가 표준 인증 실패 공정을 수행하도록 유도하고, 성공 시 커스텀 DTO를 반환해 세션을 형성함
5. 중복 체크가 포함된 회원가입 서비스 및 UI 로직 완성
- 커스텀 예외(MidExistException): 인터페이스 내부에 아이디 중복 전용 예외를 선언하여 가입 로직의 가독성과 안정성을 높임
- 서비스 구현: existsById를 통해 가입 전 아이디 중복 여부를 검사하고, 가입 시 ModelMapper로 DTO를 엔티티로 변환하여 저장함
- 사용자 피드백: 컨트롤러에서 addFlashAttribute로 전달한 에러 메시지를 join.html의 자바스크립트가 감지하여 중복 시 알림창(alert)을 띄우는 사용자 친화적 UI를 구축함
[요약]
JPA 연동 실무 회원 엔티티 설계 및 중복 방지 회원가입 시스템 구축
1. @ElementCollection과 @EntityGraph를 이용한 효율적인 다중 권한 회원 모델링
2. PasswordEncoder를 적용한 대량 테스트 데이터 생성 및 DB 조회 성능 최적화
3. 시큐리티 User 클래스를 확장한 커스텀 DTO 설계로 세션 데이터 활용도 증대
4. 실제 DB 회원 정보를 기반으로 한 CustomUserDetailsService 인증 로직 전환
5. 아이디 중복 체크 예외 처리 및 FlashAttribute 기반의 회원가입 피드백 시스템 완성
domain 패키지 안에 Member 클래스를 만듬
Member
사용자의 기본 정보와 권한 목록을 포함하는 회원 정보 엔티티를 정의하고 관리하는 로직임.
- 식별자 및 기본 정보 설정: 아이디(mid)를 PK로 사용하며 비밀번호, 이메일, 탈퇴 여부(del), 소셜 로그인 여부(social) 등의 필드를 구성함.
- 권한 집합 관리: @ElementCollection을 사용하여 별도의 테이블 없이 MemberRole의 집합(Set)을 관리하며, 기본값으로 지연 로딩(LAZY)을 설정해 성능을 최적화함.
- 가변 필드 수정 메서드: 비밀번호, 이메일, 소셜 정보 등 변경 가능한 필드들에 대해 각각 전용 change 메서드를 제공하여 객체의 캡슐화를 유지함.
- 권한 제어 편의 기능: 새로운 권한을 추가하는 addRole과 기존 권한을 모두 초기화하는 clearRoles 메서드를 통해 사용자 권한을 동적으로 제어함.
package com.example.b01.domain;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import lombok.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Member extends BaseEntity {
@Id
private String mid;
private String mpw;
private String email;
private boolean del;
private boolean social;
@ElementCollection(fetch = FetchType.LAZY)
@Builder.Default
private Set<MemberRole> roleSet = new HashSet<>();
public void changePassword(String mpw) {
this.mpw= mpw;
}
public void changeEmail(String email) {
this.email = email;
}
public void changeDel(boolean del) {
this.del = del;
}
public void addRole(MemberRole memberRole) {
this.roleSet.add(memberRole);
}
public void clearRoles() {
this.roleSet.clear();
}
public void changeSocial(boolean social) {
this.social = social;
}
}
위의 코드에서 사용한 @ElementCollection를 값 타입 컬렉션이라고 한다
값 타입 컬렉션
값 타입을 컬렉션에 담아서 사용할 때, 해당 컬렉션을 값 타입 컬렉션이라 합니다.
(@OneToMany 처럼 엔티티를 컬렉션으로 사용하는 것이 아닌,
Integer, String, 임베디드 타입 같은 값 타입을 컬렉션으로 사용)
관계형 데이터베이스는 컬렉션을 담을 수 있는 구조가 없다.
따라서 이를 저장하기 위해서는 별도의 테이블을 만들어서 저장해야 함.
이때 값 타입 컬렉션은 개념적으로 보면 1대 N 관계.
(그리고 값 타입을 저장하는 테이블은 값 타입을 소유한 엔티티의 기본 키와 모든 값 타입 필드를 묶어서 PK로 사용하며,
엔티티의 기본 키를 PK겸 FK로 사용.)
@ElementCollection
값 타입 컬렉션을 매핑할 때 사용합니다.
RDB에는 컬렉션과 같은 형태의 데이터를 컬럼에 저장할 수 없기 때문에, 별도의 테이블을 생성하여 컬렉션을 관리해야 합니다.
이때 해당 필드가 컬렉션 객체임을 JPA에게 알려주는 어노테이션이 @ElementCollection 입니다.
@Entity가 아닌 Basic Type이나 Embeddable Class로 정의된 컬렉션을 테이블로 생성하며 One-To-Many 관계로 다룸
@CollectionTable
@ElementCollection은 @CollectionTable과 함께 사용합니다.
@CollectionTable은 값 타입 컬렉션을 매핑할 테이블에 대한 정보를 지정하는 역할을 수행합니다.
만약 이를 생략한다면 기본값을 이용하여 매핑합니다.
기본값 : {엔티티이름}_{컬렉션 필드 이름}
권한을 부여하는 MemberRole Enum 클래스를 추가

MemberRole
시스템 내 사용자의 권한 등급을 구분하기 위해 사용하는 상수를 정의한 열거형(Enum) 클래스
package com.example.b01.domain;
public enum MemberRole {
USER, ADMIN;
}
MemberRepository interface 생성
MemberRepository
사용자의 아이디와 권한(Role) 정보를 한 번의 쿼리로 조회하여 스프링 시큐리티 인증 시스템에 제공하는 로직임.
- 즉시 로딩 최적화: @EntityGraph를 사용하여 지연 로딩 설정인 roleSet을 한 번에 조회함으로써 성능 저하의 주원인인 N+1 문제 방지함.
- 소셜 계정 필터링: m.social = false 조건을 명시하여 소셜 로그인 사용자가 아닌 일반 DB 로그인 사용자 데이터만 선별함.
- 사용자 정의 쿼리: @Query 어노테이션과 JPQL을 활용해 특정 아이디(mid)를 기준으로 회원을 검색하는 명시적 조회 로직 구현함.
- 데이터 안전성 확보: 조회 결과가 없는 상황에 대비해 Optional 타입을 사용하여 인증 과정에서의 Null 관련 예외 발생을 사전에 차단
package com.example.b01.repository;
import com.example.b01.domain.Member;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, String> {
// JPA에서 지연 로딩(LAZY)으로 설정된 연관 관계를 즉시 로딩(EAGER)처럼 가져오도록 지정
@EntityGraph(attributePaths = "roleSet") // N+1 문제를 방지
@Query("select m from Member m where m.mid = :mid and m.social = false ")
Optional<Member> getWithRoles(String mid);
}
MemberRepositoryTests 클래스 생성
MemberRepositoryTests
암호화된 비밀번호와 권한 설정을 포함한 테스트용 회원 데이터 100개를 데이터베이스에 일괄 저장하는 로직임.
- 암호화 적용: PasswordEncoder를 사용하여 모든 회원의 비밀번호를 "1111"로 안전하게 암호화한 뒤 저장함.
- 테스트 데이터 생성: IntStream을 활용해 member1부터 member100까지
규칙적인 아이디와 이메일을 가진 계정을 대량으로 생성함. - 다중 권한 부여: 모든 사용자에게 기본적으로 USER 권한을 부여하며,
특정 번호(90번 이상)부터는 ADMIN 권한을 추가로 할당함. - 영속성 저장: 빌더 패턴으로 생성된 Member 객체를
memberRepository.save()를 통해 실제 DB 테이블에 물리적으로 저장함.
package com.example.b01.repository;
import com.example.b01.domain.Member;
import com.example.b01.domain.MemberRole;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.stream.IntStream;
@SpringBootTest
@Log4j2
public class MemberRepositoryTests {
@Autowired
private MemberRepository memberRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Test
public void insertMembers(){
IntStream.rangeClosed(1,100).forEach(i -> {
Member member = Member.builder()
.mid("member" + i)
.mpw(passwordEncoder.encode("1111"))
.email("email"+i+"@aaa.bbb")
.build();
member.addRole(MemberRole.USER);
if (i >= 90) {member.addRole(MemberRole.ADMIN);}
memberRepository.save(member);
});
}
}
실행하면
member와 member_role_set 테이블이 자동 생성된다
근데 Spring security에서 mpw를 1111로 설정했어도 암호화를 해서 알아서 설정된다.
아래에
사용자 정의 쿼리 메서드를 호출하여 특정 회원의 상세 정보와 권한 목록이 정상적으로 조회되는지 검증하는 로직
- 커스텀 조회 실행: memberRepository의 getWithRoles 메서드를 사용하여
아이디가 "member100"인 회원의 엔티티와 연관된 권한 데이터를 동시에 가져옴. - 예외 처리 및 객체 추출: Optional의 orElseThrow를 사용하여 데이터가 존재하지 않을 경우 예외를 발생시키고,
존재할 경우에만 Member 객체를 안전하게 꺼냄. - 데이터 로그 출력: 조회된 Member 객체의 기본 정보와 권한 집합(roleSet)을 로그로 출력하여
DB 데이터와의 일치 여부를 확인함. - 권한 목록 열거: roleSet에 포함된 개별 MemberRole을 반복문으로 순회하며
각 권한의 명칭(USER, ADMIN 등)을 최종적으로 확인함.
@Test
public void testRead() {
Optional<Member> result = memberRepository.getWithRoles("member100");
Member member = result.orElseThrow();
log.info(member);
log.info(member.getRoleSet());
member.getRoleSet().forEach(memberRole -> log.info(memberRole.name()));
}
테스트 코드를 추가
실행하면
회원의 상세 정보와 권한 목록이 정상적으로 조회되는지 검증해서 로그를 띄움
원래 dto패키지가 있지만 security 패키지 안에 dto를 추가하고 MemberSecurityDTO 클래스를 만든다
MemberSecurityDTO
스프링 시큐리티의 인증 객체인 User 클래스를 상속받아,
시스템 내부에서 필요한 추가적인 사용자 정보를 담을 수 있도록 커스터마이징한 DTO 로직임.
- 시큐리티 표준 확장: 스프링 시큐리티에서 인증된 사용자를 표현하는 User 클래스를 상속하여, 시큐리티 시스템이 인식할 수 있는 규격과 실제 DB 데이터를 통합함.
- 커스텀 필드 추가: 시큐리티 기본 정보(ID, PW, 권한) 외에 이메일(email), 삭제 여부(del), 소셜 가입 여부(social) 등 비즈니스 로직에 필요한 필드들을 추가로 정의함.
- 생성자를 통한 데이터 바인딩: 부모 클래스(super)의 생성자를 호출하여 인증에 필수적인 ID, PW, 권한 목록을 먼저 할당하고, 나머지 추가 필드들을 객체 생성 시점에 초기화함.
- 인증 객체의 다목적 활용: 로그인 완료 후 세션에 저장되어, 컨트롤러나 서비스 계층에서 현재 사용자의 상세 정보를 별도의 DB 조회 없이 즉시 꺼내 쓸 수 있도록 지원
package com.example.b01.security.dto;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public class MemberSecurityDTO extends User {
private String mid;
private String mpw;
private String email;
private boolean del;
private boolean social;
public MemberSecurityDTO(String username, String password, String email,
boolean del, boolean social,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.mid = username;
this.mpw = password;
this.email = email;
this.del = del;
this.social = social;
}
}
위의 코드중
Collection<? extends GrantedAuthority> authorities
에 대한 설명
스프링 시큐리티가 사용자의 권한을 인식하고 인가 처리를 할 수 있도록 인가된 권한 목록을 보관하는 리스트 역할임.
- 시큐리티 표준 규격 준수: 스프링 시큐리티 내부에서 권한을 처리하기 위해 반드시 필요한
GrantedAuthority 인터페이스의 구현체들을 담는 표준 보관소임. - 다양한 권한 타입 수용: 와일드카드(? extends)를 사용하여 ROLE_USER, ROLE_ADMIN 같은
단순 문자열 권한뿐만 아니라, 특정 조건이나 계층 구조를 가진 다양한 권한 객체들을 유연하게 담을 수 있음. - 접근 제어의 근거 데이터: 컨트롤러의 @PreAuthorize나 시큐리티 설정(hasRole)에서
특정 페이지 접근 허용 여부를 결정할 때, 이 리스트에 담긴 권한들을 대조하여 승인 여부를 판단함. - 부모 클래스 필수 데이터: 상속받은 User 클래스의 생성자에 이 데이터를 넘겨주어야만
시큐리티가 "이 사용자는 어떤 일을 할 수 있는가"를 최종적으로 인지하게 됨.
CustomUserDetailsService
원래 있던 코드의 부분을 주석처리 하고 새로 작성
데이터베이스에서 사용자 정보를 조회하여 스프링 시큐리티의 인증 규격에 맞는
UserDetails 객체로 변환해주는 핵심 보안 로직임.
- 사용자 정보 조회: MemberRepository의 getWithRoles 메서드를 호출하여 입력받은
username에 해당하는 회원 정보와 권한 데이터를 DB에서 가져옴. - 예외 및 인증 실패 처리: 조회 결과가 존재하지 않을 경우
UsernameNotFoundException을 발생시켜 스프링 시큐리티가 인증 실패 공정을 진행하도록 유도함. - 권한 규격 변환: DB의 MemberRole 데이터를 스프링 시큐리티가 인식할 수 있는
SimpleGrantedAuthority 형태로 변환하며, 이때 관례에 따라 "ROLE_" 접두어를 결합함. - 인증 객체 생성 및 반환: 최종적으로 사용자의 상세 정보와 권한 목록을 담은
MemberSecurityDTO를 생성하여 반환함으로써 로그인을 완료하고 세션을 형성함
package com.example.b01.security;
import com.example.b01.domain.Member;
import com.example.b01.repository.MemberRepository;
import com.example.b01.security.dto.MemberSecurityDTO;
// import jdk.dynalink.Operation; // <-- 삭제 (잘못된 임포트)
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Optional; // <-- 추가
import java.util.stream.Collectors;
@Log4j2
@Service
@RequiredArgsConstructor // <-- 주석 해제 (memberRepository 주입을 위해 필요)
public class CustomUserDetailsService implements UserDetailsService {
// private PasswordEncoder passwordEncoder;
// public CustomUserDetailsService() {
// this.passwordEncoder = new BCryptPasswordEncoder();
// }
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("loadUserByUsername: " + username);
// UserDetails userDetails = User.builder()
// .username("user1")
// .password("1111")
// .password(passwordEncoder.encode("1111")) // 패스워트 인코드
// .authorities("ROLE_USER")
// .build();
// return userDetails;
// return null;
// Operation 대신 Optional 사용
Optional<Member> result = memberRepository.getWithRoles(username); // DB에서 username(mid)로 회원 조회
if(result.isEmpty()) { // 해당 아이디를 가진 사용자가 없다면...Spring Security가 예외를 감지해서 인증 실패 처리
throw new UsernameNotFoundException("username not found...");
}
Member member = result.get();
MemberSecurityDTO memberSecurityDTO =
new MemberSecurityDTO(
member.getMid(),
member.getMpw(),
member.getEmail(),
member.isDel(),
false,
member.getRoleSet()
.stream().map(memberRole ->
new SimpleGrantedAuthority("ROLE_" + memberRole.name())) // Role_ -> ROLE_ (관례상 대문자)
.collect(Collectors.toList()));
log.info("memberSecurityDTO");
log.info(memberSecurityDTO);
return memberSecurityDTO;
}
}
실행해서 이전에 임의로 만들었던 member1~100번까지중에 http://localhost:8080/board/register로 접속하고
http://localhost:8080/member/login로 리다이렉트 되서 member1로 로그인을 하면
게시글도 member1로 작성이 되는것을 볼 수 있다.
dto 패키지 안에 MemberJoinDTO를 만들어준다
MemberJoinDTO
package com.example.b01.dto;
import lombok.Data;
@Data
public class MemberJoinDTO {
private String mid;
private String mpw;
private String email;
private boolean del;
private boolean social;
}
MemberController에 코드를 추가
회원가입 페이지를 요청자에게 보여주고,
입력된 가입 정보를 처리한 뒤 게시판 목록으로 리다이렉트하는 페이지 이동 및 데이터 처리 로직임.
- 가입 페이지 호출: @GetMapping("/join")을 통해 사용자가 회원가입 화면에 접속했을 때 해당 뷰(View) 파일을 출력하도록 제어함.
- 가입 데이터 수신: 사용자가 입력한 회원 정보를 MemberJoinDTO 객체로 자동 바인딩하여 수집하며, 내부 로그를 통해 전달된 값의 정합성을 확인함.
- 처리 후 화면 전환: 회원가입 로직이 완료된 후 redirect:/board/list를 반환하여 사용자의 브라우저를 게시판 목록 페이지로 즉시 이동시킴.
- 전송 방식 분리: 동일한 URL(/join)에 대해 조회(GET)와 등록(POST) 방식을 각각 선언함으로써 RESTful한 컨트롤러 구조를 유지함
@GetMapping("/join")
public void joinGET() {
log.info("join get...");
}
@PostMapping("/join")
public String joinPOST(MemberJoinDTO memberJoinDTO) {
log.info("join post...");
log.info(memberJoinDTO);
return "redirect:/board/list";
}
join.html을 만들어줌
join.html
사용자가 회원가입을 위해 아이디, 비밀번호, 이메일을 입력하고
서버로 전송할 수 있도록 구성된 타임리프(Thymeleaf) 기반의 HTML 화면 로직임.
- 레이아웃 시스템 적용: layout:decorate 속성을 사용하여 공통 디자인 틀인 basic.html을 상속받으며, layout:fragment="content" 영역에 회원가입 전용 UI를 구현함.
- 입력 폼 구조화: <form> 태그를 통해 입력된 데이터를 /member/join 경로로 POST 방식 전송하도록 설정하고, 각 입력창(input)에 DTO 필드명과 일치하는 name 속성을 부여함.
- 사용자 인터페이스(UI) 구성: 부트스트랩 클래스를 활용해 MID(아이디), MPW(비밀번호), EMAIL 입력란을 깔끔한 카드 형태로 배치하여 시각적 가독성을 높임.
- 전송 및 초기화 제어: submit 타입 버튼으로 폼 데이터를 서버에 제출하거나, reset 버튼을 통해 입력했던 내용을 한 번에 지울 수 있는 조작 기능을 제공
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/basic.html}">
<head>
<title>Member Join Page</title>
</head>
<div layout:fragment="content">
<div class="row mt-3">
<div class="col">
<div class="card">
<div class="card-header">
JOIN
</div>
<div class="card-body">
<form id="registerForm" action="/member/join" method="post">
<div class="input-group mb-3">
<span class="input-group-text">MID</span>
<input type="text" name="mid" class="form-control">
</div>
<div class="input-group mb-3">
<span class="input-group-text">MPW</span>
<input type="password" name="mpw" class="form-control">
</div>
<div class="input-group mb-3">
<span class="input-group-text">EMAIL</span>
<input type="email" name="email" class="form-control">
</div>
<div class="my-4">
<div class="float-end">
<button type="submit" class="btn btn-primary submitBtn">Submit</button>
<button type="reset" class="btn btn-secondary">Reset</button>
</div>
</div>
</form>
</div><!--end card body-->
</div><!--end card-->
</div><!-- end col-->
</div><!-- end row-->
</div>
<script layout:fragment="script" th:inline="javascript">
</script>
실행하고 http://localhost:8080/member/join 주소로 들어가서
내용을 입력한다. 지금 전송은 안되지만 로그에 join post....가 뜬다
MemberService interface 클래스를 생성한다
MemberService
회원가입 비즈니스 로직을 정의하며, 특히 중복된 아이디 가입 시도에 대한 예외 처리를 규격화한 인터페이스 로직임.
- 서비스 명세 정의: 회원가입을 수행하는 join 메서드를 선언하여, 실제 구현체(ServiceImpl)가 어떤 기능을 수행해야 하는지 표준 가이드를 제시함.
- 커스텀 예외 선언: 아이디 중복 상황을 명확히 구분하기 위해 인터페이스 내부에 MidExistException 정적 클래스를 정의하여 예외 처리의 가독성을 높임.
- 중복 검증 강제: join 메서드에 throws MidExistException을 명시함으로써, 호출하는 컨트롤러 계층에서 반드시 아이디 중복 상황에 대한 방어 로직을 작성하도록 강제함.
- 데이터 전달 규격: 가입에 필요한 데이터들을 MemberJoinDTO 객체 단위로 전달받아 계층 간 데이터 이동을 구조화
package com.example.b01.service;
import com.example.b01.dto.MemberJoinDTO;
public interface MemberService {
// "이미 존재하는 아이디(mid)"일 때 발생시키는 커스텀 예외
static class MidExistException extends Exception{ }
void join(MemberJoinDTO memberJoinDTO) throws MidExistException;
}
MemberService를 implements 하는 MemberServiceImpl 클래스를 생성한다
MemberServiceImpl
package com.example.b01.service;
import com.example.b01.domain.Member;
import com.example.b01.domain.MemberRole;
import com.example.b01.dto.MemberJoinDTO;
import com.example.b01.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Log4j2
@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
private final ModelMapper modelMapper;
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
@Override
public void join(MemberJoinDTO memberJoinDTO) throws MidExistException{
String mid = memberJoinDTO.getMid();
boolean exist = memberRepository.existsById(mid); // 해당 id와 회원이 DB에 존재여부 확인
if(exist){ throw new MidExistException(); }
Member member = modelMapper.map(memberJoinDTO, Member.class);
member.changePassword(passwordEncoder.encode(memberJoinDTO.getMid()));
member.addRole(MemberRole.USER);
log.info("=======================");
log.info(member);
log.info(member.getRoleSet());
memberRepository.save(member);
}
}
작성한 service를 controller에 적용 하기 위해
MemberController안에 memberService(의존성)을 주입하고
@PostMapping("/join") 안에 아래 코드 추가
회원가입 요청을 처리할 때 서비스 계층과 협력하여 중복 아이디를 체크하고,
결과에 따라 서로 다른 페이지로 이동시키는 컨트롤러 로직임.
- 서비스 의존성 주입: private final MemberService를 통해 회원가입 비즈니스 로직이 담긴
서비스 객체를 주입받아 컨트롤러에서 기능을 사용할 수 있도록 준비함. - 가입 로직 실행 및 예외 처리: try-catch 문을 사용하여 가입을 시도하며,
만약 중복된 아이디가 존재하여 MidExistException이 발생할 경우 가입 페이지로 되돌림. - 일회성 데이터 전달: 중복 오류 발생 시 addFlashAttribute를 사용하여 브라우저를 새로고침하면
사라지는 일회성 에러 메시지("mid")를 가입 화면에 전달함. - 성공 시 페이지 전환: 회원가입이 성공하면 URL 파라미터에 성공 결과(result=SUCCESS)를 붙여
로그인 페이지(/member/login)로 사용자를 이동시킴.
//의존성 주입
private final MemberService memberService;
@PostMapping("/join")
public String joinPOST(MemberJoinDTO memberJoinDTO,
RedirectAttributes redirectAttributes) {
log.info("join post...");
log.info(memberJoinDTO);
try {
memberService.join(memberJoinDTO);
} catch (MemberService.MidExistException e) {
redirectAttributes.addFlashAttribute("error", "mid");
return "redirect:/member/join";
}
redirectAttributes.addAttribute("result", "SUCCESS");
return "redirect:/member/login"; // 회원 가입 후 로그인
// return "redirect:/board/list";
join.html 코드 아래 script부분 수정
서버로부터 전달받은 에러 데이터를 확인하여 사용자에게 아이디 중복 상황을 알림창(alert)으로 안내하는 프론트엔드 제어 로직임.
- 서버 데이터 수신: 타임리프의 [[${error}]] 문법을 사용하여 컨트롤러에서 addFlashAttribute로 보낸 에러 정보를 자바스크립트 변수로 안전하게 받아옴.
- 조건부 로직 실행: 전달받은 error 값이 존재하고 그 내용이 'mid'인 경우에만 특정 동작이 수행되도록 방어 코드를 구성함.
- 사용자 알림 피드백: 중복된 아이디가 있을 때 "동일한 MID를 가진 계정이 존재합니다."라는 메시지를 띄워 가입 실패 원인을 사용자에게 즉각적으로 인지시킴.
- 일회성 메시지 처리: 리다이렉트 시 전달된 데이터이므로 새로고침 시 메시지가 다시 나타나지 않도록 설계되어 중복 알림을 방지
<script layout:fragment="script" th:inline="javascript">
const error = [[${error}]]
if (error && error === 'mid') {
alert("동일한 MID를 가진 계정이 존재합니다.")
}
</script>
실행해서
http://localhost:8080/member/join 에 접속 후 회원가입
로그인을 하면 main페이지로 넘어가고 db에도 추가된다
같은 아이디로 회원가입을 하면 동일한 MID를 가진 계정이 존재합니다.라고 alert창이 뜬다.
'대우개발원 수업 내용 > spring boot, framework' 카테고리의 다른 글
| 자바 스프링 부트 17일차 /b01Security 카카오 OAuth2 소셜 로그인 연동 (0) | 2026.04.30 |
|---|---|
| 자바 스프링 부트 15일차 / b01Security Spring Security 기반 자동 로그인 구현 및 사용자별 보안 로직 강화 (0) | 2026.04.28 |
| 자바 스프링 부트 14일차 /b01Security 회원가입(Security) 핵심 설정 및 커스텀 인증 시스템 구축 (0) | 2026.04.27 |
| 자바 스프링 부트 13일차 b01Upload (1) | 2026.04.24 |
| 자바 스프링 부트 13일차 b01Upload (1) | 2026.04.23 |





















