티켓 예매 사이트 - 1

대용량 DB처리를 해보기 위해서 간략하게 티켓사이트를 만드려고 한다.


티켓 예매 사이트의 프로젝트 구조를 정리하고 로그인과 회원가입을 구현했다. 프론트엔드는 Next.js,
백엔드는 Spring Boot로 구성했고, 개발용 데이터베이스는 Docker Compose로 MariaDB와 Adminer를 띄우도록 설정했다.

회원가입 요청을 받고 검증하기

회원가입 요청은 MemberController에서 받는다.

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

      @PostMapping
      @ResponseStatus(HttpStatus.CREATED)
      public SignupResponse signup(
              @Valid @RequestBody SignupRequest request
      ) {
          return new SignupResponse(
              memberService.signup(
                  request.name(),
                  request.email(),
                  request.password()
              )
          );
      }

      public record SignupResponse(Long id) {}
  }

요청 DTO에는 다음과 같이 조건을 지정했다.

public record SignupRequest(
      @NotBlank(message = "이름을 입력해 주세요.")
      @Size(max = 100, message = "이름은 100자 이하로 입력해 주세요.")
      String name,

      @NotBlank(message = "이메일을 입력해 주세요.")
      @Email(message = "올바른 이메일을 입력해 주세요.")
      @Size(max = 254, message = "이메일이 너무 깁니다.")
      String email,

      @NotBlank(message = "비밀번호를 입력해 주세요.")
      @Size(
          min = 8,
          max = 72,
          message = "비밀번호는 8자 이상 72자 이하로 입력해 주세요."
      )
      String password
  ) {}

회원가입 서비스에서 데이터를 정리하고 저장하기

@Transactional
  public Long signup(String name, String email, String password) {
      email = email.trim().toLowerCase(Locale.ROOT);

      if (password.getBytes(StandardCharsets.UTF_8).length > 72) {
          throw new IllegalArgumentException(
              "비밀번호는 UTF-8 기준 72바이트 이하여야 합니다."
          );
      }

      if (memberRepository.existsByEmail(email)) {
          throw new DuplicateEmailException();
      }

      String encodedPassword = passwordEncoder.encode(password);

      Member member = new Member(
          name.trim(),
          email,
          encodedPassword
      );

      Member savedMember = memberRepository.save(member);

      return savedMember.getId();
  }

로그인은 Spring Security에 연결하기

로그인 처리는 SecurityConfig에서 설정했다.

.formLogin(form -> form
      .loginProcessingUrl("/api/auth/login")
      .usernameParameter("email")
      .successHandler((request, response, authentication) ->
          response.setStatus(204)
      )
      .failureHandler((request, response, error) ->
          jsonError(
              response,
              401,
              "이메일 또는 비밀번호가 올바르지 않습니다."
          )
      )
      .permitAll()
  )

로그인 요청 주소는 /api/auth/login으로 지정하고, 사용자 식별 필드는 email로 설정했다.
이 주소로 들어오는 로그인 요청은 Spring Security가 처리한다. 프로젝트의 AuthController에는
로그인 POST 메서드가 없는 이유다.

세션 쿠키와 CORS 설정 연결하기

로그인 이후 요청에서도 로그인 상태를 유지하기 위해 공통 fetch 함수에 다음 설정을 넣었다.

response = await fetch(`${API_URL}${path}`, {
    ...init,
    credentials: "include",
    cache: "no-store",
  });

현재 프론트엔드는 localhost:3000, 백엔드는 localhost:8080을 사용한다. 포트가 다르므로 출처도 다르다.백엔드에서는 이에 맞춰 CORS를 설정했다.

next.config.ts로 해결

import type { NextConfig } from "next";

const BACKEND_URL = process.env.INTERNAL_BACKEND_URL ?? "http://localhost:8080";

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      {
        source: "/api/:path*",
        destination: `${BACKEND_URL}/api/:path*`,
      },
    ];
  },
};

export default nextConfig;

예외 응답의 형식 맞추기

회원가입 과정에서 발생하는 예외는 GlobalExceptionHandler에서 처리했다.

@RestControllerAdvice
  public class GlobalExceptionHandler {
      public record ErrorResponse(String message) {}

      @ExceptionHandler(DuplicateEmailException.class)
      public ResponseEntity<ErrorResponse> duplicate(
              DuplicateEmailException error
      ) {
          return ResponseEntity.status(409)
              .body(new ErrorResponse(error.getMessage()));
      }
  }


댓글 0

댓글을 불러오는 중입니다.