[TIL] Spring Boot - 회원가입 기능 구현하기

냠냠빈·2025년 1월 23일

들어가며

오늘은 Spring Boot로 회원가입 시스템을 구현해보려고 합니다.

왜 계층 구조가 중요할까?

실제 서비스를 개발하다 보면, "아 이거 그냥 Controller에서 한번에 다 처리하면 되는 거 아닌가?"라는 생각이 들 수 있습니다.

Controller가 비대해지는 경우

예를 들어, 회원가입 시 다음과 같은 코드를 작성했다고 해봅시다:

@PostMapping("/signup")
public String signup(@RequestBody SignupRequest request) {
    // 비밀번호 암호화
    String encodedPassword = passwordEncoder.encode(request.getPassword());
    
    // 중복 체크
    if (userRepository.existsByEmail(request.getEmail())) {
        throw new DuplicateEmailException();
    }
    
    // 이메일 발송
    emailService.sendVerificationEmail(request.getEmail());
    
    // 유저 저장
    User user = new User(request.getEmail(), encodedPassword);
    userRepository.save(user);
    
    return "redirect:/";
}

이런 코드가 어떤 문제를 일으킬까요?

  1. 테스트가 어려워집니다 - 하나의 메서드에서 너무 많은 일을 하고 있어서, 각각의 기능을 독립적으로 테스트하기가 힘들어요.
  2. 코드 재사용이 불가능합니다 - 다른 기능(예: 관리자 페이지에서의 회원 추가)에서 같은 로직을 사용하고 싶다면?
  3. 유지보수가 어려워집니다 - 비즈니스 로직이 변경될 때마다 컨트롤러를 수정해야 해요.

Service 계층의 실제 활용

그래서 우리는 Service 계층을 다음과 같이 분리합니다:

@Service
@Transactional
public class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final EmailService emailService;

    public User signup(SignupRequest request) {
        // 비즈니스 로직 집중화
        validateSignupRequest(request);
        User user = createUser(request);
        sendVerificationEmail(user);
        return user;
    }

    private void validateSignupRequest(SignupRequest request) {
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new DuplicateEmailException("이미 사용중인 이메일입니다");
        }
    }

    private User createUser(SignupRequest request) {
        String encodedPassword = passwordEncoder.encode(request.getPassword());
        return userRepository.save(new User(request.getEmail(), encodedPassword));
    }
}

이렇게 하면 어떤 장점이 있을까요?

  1. 비즈니스 로직의 응집도가 높아집니다 - 회원가입과 관련된 모든 로직이 한 곳에 모여있어요.
  2. 트랜잭션 관리가 용이합니다 - @Transactional로 모든 데이터 처리가 원자적으로 이루어져요.
  3. 테스트가 쉬워집니다 - 각 메서드별로 단위 테스트 작성이 가능해요.

1. 기본 설계 원칙

회원가입 시스템을 구현할 때는 다음 세 가지를 특히 중요하게 고려해야 합니다

  1. 계층 분리: 유지보수와 테스트를 용이하게 하기 위해
  2. 데이터 검증: 잘못된 데이터 유입 방지
  3. 보안: 사용자 정보 보호

이제 각 계층별로 어떻게 구현하는지 살펴보겠습니다.


2. Entity 계층 구현

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String nickname;

    @Enumerated(EnumType.STRING)
    private Role role;

    @Builder
    public Member(String email, String password, String nickname) {
        this.email = email;
        this.password = password;
        this.nickname = nickname;
        this.role = Role.USER;
    }
}

여기서 주목할 점은:

  • @Column(unique = true)로 이메일 중복 방지
  • Protected 생성자로 무분별한 객체 생성 제한
  • Builder 패턴 사용으로 객체 생성 과정 명확화

3. Repository 계층

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByEmail(String email);
    boolean existsByEmail(String email);
    boolean existsByNickname(String nickname);
}

단순해 보이지만, 실제로는 Spring Data JPA가 많은 일을 해주고 있습니다. 메소드 이름만으로 쿼리가 자동 생성되죠.


4. Service 계층

Service 계층은 실제 비즈니스 로직이 동작하는 곳입니다:

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public Long signUp(SignUpRequest request) {
        validateSignUpInfo(request);
        
        Member member = Member.builder()
            .email(request.getEmail())
            .password(passwordEncoder.encode(request.getPassword()))
            .nickname(request.getNickname())
            .build();
            
        Member savedMember = memberRepository.save(member);
        eventPublisher.publishEvent(new MemberSignedUpEvent(savedMember));
        
        return savedMember.getId();
    }

    private void validateSignUpInfo(SignUpRequest request) {
        if (memberRepository.existsByEmail(request.getEmail())) {
            throw new DuplicateMemberException("이미 가입된 이메일입니다.");
        }
        
        if (memberRepository.existsByNickname(request.getNickname())) {
            throw new DuplicateMemberException("이미 사용중인 닉네임입니다.");
        }
    }
}

5. Controller 계층

@RestController
@RequestMapping("/api/members")
@RequiredArgsConstructor
public class MemberController {
    private final MemberService memberService;

    @PostMapping("/signup")
    public ResponseEntity<ApiResponse<Long>> signUp(
            @Valid @RequestBody SignUpRequest request) {
        Long memberId = memberService.signUp(request);
        return ResponseEntity.ok(ApiResponse.success(memberId));
    }

    @ExceptionHandler(DuplicateMemberException.class)
    public ResponseEntity<ApiResponse<Void>> handleDuplicateMemberException(
            DuplicateMemberException e) {
        return ResponseEntity.badRequest()
            .body(ApiResponse.error(e.getMessage()));
    }
}


html도 간단하게 작성하면 이렇게 회원가입 폼을 볼 수 있습니다.


6. 실전에서 고려해야 할 점들

6.1 트랜잭션 관리

@Transactional(readOnly = true)
public class MemberService {
    @Transactional
    public Long signUp(SignUpRequest request) {
        // 회원가입 로직
    }
}

읽기 전용 트랜잭션을 기본으로 설정하고, 쓰기가 필요한 메서드에만 @Transactional을 붙여 성능을 최적화합니다.

6.2 비밀번호 암호화

@Configuration
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}

6.3 이벤트 기반 확장

@Component
@RequiredArgsConstructor
public class MemberSignUpEventListener {
    private final EmailService emailService;
    
    @EventListener
    @Async
    public void handleSignUpEvent(MemberSignedUpEvent event) {
        emailService.sendWelcomeEmail(event.getMember());
    }
}

이벤트 기반으로 구현하면 회원가입 후의 부가 기능(이메일 발송, 포인트 지급 등)을 유연하게 추가할 수 있습니다.


주의할 점

  1. 비밀번호 암호화는 반드시 필요
  2. 이메일 중복 체크는 동시성을 고려해서 구현
  3. 트랜잭션 범위를 적절히 설정
  4. 예외 처리를 꼼꼼히 구현

이러한 점들을 고려하면서 개발하면 실제 서비스에서도 잘 동작하는 회원가입 시스템을 만들 수 있습니다.


참고 문헌

점프 투 스프링부트 - 회원가입 기능

profile
다 먹어버릴거야!

0개의 댓글