수달이네 기술 블로그

3. MVC패턴을 이용한 로그인 회원가입 구 본문

웹/Spring

3. MVC패턴을 이용한 로그인 회원가입 구

슬픈 수달이 2025. 12. 12. 00:27

회원구현부는 최대한 기능을 상세히 나누어 구현한다.

1. DB설계 & Entity 만들기

디비버 상에서 (오라클로 구현)

CREATE TABLE USERS(
	user_id NUMBER,
	login_id varchar2(50) NOT NULL,
	password varchar2(200) NOT NULL,
	gender varchar2(10),
	email varchar2(100),
	address varchar2(200),
	address_detail varchar2(200),
	zipcode varchar2(10),
	CONSTRAINT pk_users PRIMARY KEY (user_id)
	);
	

CREATE SEQUENCE seq_users;
  • 로그인 시 필요할 데이터 베이스를 설계 후 데이터베이스에 만든다.
  • 해당 결과물을 기반으로 JDBC에 구현예정

2. dto/UserDTO.java기반으로 구현

package com.example.finalApp.dto;

import lombok.Data;

@Data
public class UserDTO {
//	CREATE TABLE USERS(
//			user_id NUMBER,
//			login_id varchar2(50) NOT NULL,
//			password varchar2(200) NOT NULL,
//			gender varchar2(10),
//			email varchar2(100),
//			address varchar2(200),
//			address_detail varchar2(200),
//			zipcode varchar2(10),
//			CONSTRAINT pk_users PRIMARY KEY (user_id)
//			);
	
	private Long userId; //Long 레퍼클래스타입, 객체타입(Null값 저장 가능, DB의 NUMBER, BIGINT컬럼과 매핑시 주로 사용)
	private String loginId;
	private String password;
	private String gender;
	private String email;
	private String address;
	private String addressDetail;
	private String zipcode;
}
  • 롬복: @Data:를 통해 Setter, Getter, ToString등을 한번에 구현

3.repository구현

repository에선 유저의 값을 삽입하고, 가져오는 함수들을 만든다.

인터페이스

package com.example.finalApp.repository;

import java.util.Optional;

import com.example.finalApp.dto.UserDTO;

public interface UserRepository {
	//Repository가 가져야할 기능(메소드 목록)만 정의
	//구현은 jdbcUserRepository.java에서 구현
	
	//회원가입(새로운 사용자 저장: 반환값이 필요하지 않으므로 void)
	void insertUser(UserDTO user);
	//DB삽입 성공> 정상실행
	//DB삽입 실패 > 예외
	
	//로그인 시 유저번호 조회(PK)
	Optional<Long> findUserNumber(String loginId, String password);
	//로그인 할 때 로그인 성공 > 해당 사용자의 user_id(PK)를 반환
	//로그인 실패 > 아무런 값도 반환 하지 않음.
	
	//Optional<Long>을 쓰는 이유
	//로그인 성공 user_id가 존재하므로 Optional.of(값)이 반환
	//로그인 실패 일치하는 사용자가 없으므로 Optional.empty()반환
}

  • 회원가입과 로그인 시 유저번호를 조회하는 기능을 만든다.
  • Optional<Long>을 반환하는 이유는 유저번호가 있을 수도 있고, 없을수도 있기 때문에 안정적으로 반환하기 위해서 이다.

구현부

package com.example.finalApp.repository;

import java.util.Optional;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.example.finalApp.dto.UserDTO;

import lombok.RequiredArgsConstructor;

@Repository // Spring이 이 클래스를 DAO(데이터 접근 레이어)로 관리
@RequiredArgsConstructor // final 필드인 JdbcTmplate을 생성자 주입(DI)
public class jdbcUserRepository implements UserRepository {

   private final JdbcTemplate jdbc;

   // alt + shift + s + v : 오버라이딩 (재정의)
   @Override
   public void insertUser(UserDTO user) {
      // 시퀀스 번호 미리 가져오기
      Long userId = jdbc.queryForObject("SELECT SEQ_USER.NEXTVAL FROM DUAL", Long.class);
      user.setUserId(userId);

      // Insert 실행
      String sql = "INSERT INTO users (USER_ID, LOGIN_ID, PASSWORD, GENDER, EMAIL, ADDRESS, ADDRESS_DETAIL, ZIPCODE) "
            + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)";

      jdbc.update(sql, user.getUserId(), user.getLoginId(), user.getPassword(), user.getGender(), user.getEmail(),
            user.getAddress(), user.getAddressDetail(), user.getZipcode());
   }

   @Override
   public Optional<Long> findUserNumber(String loginId, String password) {
      String sql = "SELECT USER_ID FORM USERS WHERE LOGIN_ID = ? AND PASSWORD=?";

      try {
         Long userId = jdbc.queryForObject(sql, Long.class, loginId, password);
         // queryForObject : 결과가 정확히 1행일 때 그 행의 첫 컬럼을 Long으로 반환
         return Optional.ofNullable(userId);

         // 행이 있으면 Optional.ofNullable(userId) Optional로 감싸서 반환
         // USER_ID NUMBER NOT NULL이면 of로만 써도 가능함
      } catch (EmptyResultDataAccessException e) {
         // TODO Auto-generated catch block
         return Optional.empty(); //일치하는 계정이 없음
         // 행이 없으면 queryForObject가 EmptyReusltDataAccessException를 던지므로 Optional.empty()를
         // 반환 => 일치하는 사용자가 없음을 의미
      }

   }

}
  • jdbc를 이용하여 데이터베이스에서 값을 가져오는 쿼리문을 짠다.
  • insertUser에선 insert문을 이용하여 삽입,
  • findUserNumbner에선 select문을 이용하여 데이터를 찾는다.

4.UserService

비즈니스 로직, 즉 비밀번호 확인, 아이디 검사등을 담당

인터페이스

package com.example.finalApp.service;

import java.util.Optional;

import com.example.finalApp.dto.UserDTO;

public interface UserService {
	//서비스 계층: 비즈니스 로직 처리
	//회원가입 비즈니스 로직
	//비밀번호 확인, 중복 아이디 검사, 필수값 겁증, Repository를 호출하여 DB에 저장
	void join(UserDTO user, String confirmPassword);
	
	//로그인 비즈니스 로직
	//입력한 로그인 id가 존재하는지 확인, 비밀번호가 일치하는지 확인, 성공하면 사용자 번호 반환, 실패시 Optional,empty반환
	Optional<Long> login(String loginId, String password);
	
}

구현부

package com.example.finalApp.service;

import java.util.Optional;

import org.springframework.stereotype.Service;

import com.example.finalApp.dto.UserDTO;
import com.example.finalApp.repository.UserRepository;

import lombok.RequiredArgsConstructor;

@Service // 스프링이 서비스 빈으로 등록(트랜잭션, 비즈니스 로직 위치)
@RequiredArgsConstructor // final 필드(userRepository)를 매개변수로 받는 생성자 자동 생성 -> 생성자 주입(DI)
public class UserServiceImpl implements UserService {

   private final UserRepository userRepository; // 실제 DB접근을 수행하는 영속성 계층
   // private final : 반드시 필요하고, 변경되면 안되는 의존성이라는 의미
   // final이라 생성자 에서만 초기화가 가능 -> 필드 주입이 아닌 생성자 주입을 강제하는 패턴
   // NPE(NullPointerException)을 방지, 테스트 코드 작성시 의존성이 명확, 순환 참조를 발견 쉬움

   @Override
   public void join(UserDTO user, String confirmPassword) {
      // 비밀번호 확인 체크
      if (!user.getPassword().equals(confirmPassword)) {
         throw new IllegalArgumentException("비밀번호와 비밀번호 확인이 일치하지 않습니다");
      }

      // DB 저장 호출
      userRepository.insertUser(user);

   }

   @Override
   public Optional<Long> login(String loginId, String password) {
      //레포지토리에게 조회 위임
      return userRepository.findUserNumber(loginId, password);
   }

}

  • service 중 join문에선 회원가입과 관련하여 확인하므로 비밀번호와 비밀번호가 일치하는지 확인하는내용을 반환
  • login에선 레포지토리에게 조회를 위임하여 같은 아이디가 있으면 조회, 없으면 Optional.empty()를 반환

service까지 구현했으나,

user를 찾지 못함.(404: 사용자 잘못)

5. controller구현

요청 url과 다르다.

그 이유는 controller를 구현하지 않았기 때문이다.

controller를 구현하면,

package com.example.finalApp.controller;

import java.util.Optional;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.example.finalApp.dto.UserDTO;
import com.example.finalApp.service.UserService;

import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;

@Controller
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {
   private final UserService userService;

   // 요청 URL : GET /user/join => joinForm()메소드 실행
   @GetMapping("/join")
   public String joinForm(Model model) {
      if (!model.containsAttribute("user")) {
         // Model에 user가 없으면 새 UserDTO(빈 객체) 넣기
         model.addAttribute("user", new UserDTO());
      }
      return "user/join"; // templates/user/join.html
   }

   // 회원가입 처리
   // 요청 URL : POST /user/join => join() 메소드 실행
   @PostMapping("/join")
   public String join(@ModelAttribute("user") UserDTO user, @RequestParam("confirm-password") String confirmPassword,
         RedirectAttributes ra) {

      try {
         userService.join(user, confirmPassword);
         ra.addFlashAttribute("msg", "회원가입이 완료되었습니다. 로그인해주세요.");
         return "redirect:/user/login";
      } catch (Exception e) {
         ra.addFlashAttribute("error", "회원가입중 오류가 발생했습니다.");
         return "redirect:/user/join";
      }

   }

   // 요청 URL : GET /user/login => loginForm() 메소드 실행
   @GetMapping("/login")
   public String loginForm() {
      return "user/login";
   }

   // 요청 URL : POST /user/login => login() 메소드 실행
   @PostMapping("/login")
   public String login(@RequestParam("loginId") String loginId, @RequestParam("password") String password,
         HttpSession session, RedirectAttributes ra) {
      Optional<Long> userId = userService.login(loginId, password);
      if (userId.isPresent()) {
         session.setAttribute("userId", userId.get());
         session.setAttribute("loginId", userId);
         return "redirect:/board/list"; // 로그인 성공 시 게시판으로 이동
      } else {
         ra.addFlashAttribute("error", "아이디 또는 비밀번호가 틀렸습니다.");
         return "redirect:/user/login"; // 로그인 실패 시 다시 로그인 페이지로 이동
      }

   }

   // 요청 URL : GET /user/logout => logout() 메소드 실행
   @GetMapping("/logout")
   public String logout(HttpSession session) {
      session.invalidate();
      return "redirect:/user/login";
   }

}

  • controller에선 각 path의 기능을 완성한다.
  • /join에선 회원가입을 Post로 정보를 주고받는다(Post로 만들어 Get처럼 데이터가 앞에서 주고받아지는 것을 방지)
  • /login 도 마찬가지로 Post로
  • /logout에선 Get으로 매핑한다.

이후엔 게시판 구현도 해볼 것이다.

' > Spring' 카테고리의 다른 글

2. Spring으로 MVC패턴 구축하기1  (0) 2025.12.09
1. SpringBoot 기초  (0) 2025.12.08
0. SpringBoot  (1) 2025.12.04