※ 본문은 혼자 공부한 내용을 기록한 글입니다. 오개념이 있다면 댓글로 알려주세요!
2023.01.29 - [나에 대한 기록] - [독서 기록] 스프링5 프로그래밍 입문 - 최범균 저
[독서 기록] 스프링5 프로그래밍 입문 - 최범균 저
https://search.shopping.naver.com/book/catalog/32458958626 스프링5 프로그래밍 입문 : 네이버 도서 네이버 도서 상세정보를 제공합니다. search.shopping.naver.com 지난 한 주 동안 최범균님의 스프링 5 프로그래밍
krchoish.tistory.com
※ 해당 책을 참고하여 프로젝트를 진행하였습니다.
[ 1 ] MySQL에서의 Member table과 Member DTO 준비
회원가입의 정보는 MySQL의 Member table에 저장될 것이고, 계층 간 데이터 교환을 위한 객체로 Member DTO가 사용될 것이다.
(DTO란 로직을 갖지 않는 순수한 데이터 객체이며 getter와 setter만을 가진 클래스이다)
id를 Primary key로, email을 Unique key로 설정했다. 또한, 이름, 성별, 성, 생일, 비밀번호, 가입날짜를 회원가입의 정보로 받을 계획이다.
회원가입 정보를 저장하고 가져오기 위한 DTO인 Member 객체는 다음과 같다.
package dto;
import java.time.LocalDateTime;
public class Member {
private Long id;
private String email;
private String name;
private String sex;
private String birthDate;
private String password;
private LocalDateTime regdate;
public Member() {
}
public Member(String email, String name, String sex, String birthDate, String password, LocalDateTime regdate) {
this.email = email;
this.name = name;
this.sex = sex;
this.birthDate = birthDate;
this.password = password;
this.regdate = regdate;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getBirthDate() {
return birthDate;
}
public void setBirthDate(String birthDate) {
this.birthDate = birthDate;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public LocalDateTime getRegdate() {
return regdate;
}
public void setRegdate(LocalDateTime regdate) {
this.regdate = regdate;
}
}
[ 2 ] DB 연동
해당 프로젝트는 JDBC를 통해 DB를 연동하고, Spring이 제공하는 JdbcTemplate를 사용한다.
처음 JDBC를 배울 때에는 보통 DriverManager로 DB를 연동하지만, JDBC API는 DriverManager 외에 DataSource를 이용해서도 DB를 연동할 수 있다.
여기에서는 javax.sql.DataSource를 구현한 Tomcat JDBC 모듈을 사용한다.
Tomcat JDBC 모듈을 사용하는 이유는 '커넥션 풀' 때문이다. DBMS로 커넥션을 생성하는 시간은 매우 길기 때문에 DB 커넥션을 생성하는 시간은 프로그램의 전체 성능에 영향을 미친다. 따라서, 동시에 접속하는 사용자수가 많아 DB 커넥션을 많이 생성하게 되면 프로그램의 성능이 저하될 수 있다.
'커넥션 풀'은 일정 개수의 DB 커넥션을 미리 만들어두는데, DB 커넥션이 필요한 프로그램은 커넥션 풀에서 커넥션을 가져와 사용할 수 있고 사용을 마친 커넥션을 다시 풀에 반납한다. 즉, 커넥션 풀은 커넥션을 미리 생성해 두기 때문에 커넥션을 생성하는 시간을 아낄 수 있는 것이다. 커넥션 풀은 더 많은 동시 접속자를 처리할 수 있을 뿐만 아니라, 커넥션도 일정 개수로 유지하며 DBMS에 대한 부하를 일정 수준으로 낮출 수도 있다. Tomcat JDBC 모듈은 이러한 커넥션 풀을 제공한다.
Spring이 제공하는 DB 연동 기능은 DataSource를 사용해서 DB Connection을 구한다. 그러므로 DB 연동에 사용할 DataSource 객체를 Spring Bean으로 등록하고, DB 연동을 구현한 Bean 객체(DAO)는 DataSource를 주입받아 사용해야 한다.
이를 위한 MemberConfig는 다음과 같이 작성한다.
package config;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import dao.MemberDao;
import db.DBInfo;
import service.RegisterService;
@Configuration
@EnableTransactionManagement
public class MemberConfig {
@Bean(destroyMethod = "close")
public DataSource dataSource() {
DataSource ds = new DataSource();
ds.setDriverClassName(DBInfo.driverClassName);
ds.setUrl(DBInfo.url);
ds.setUsername(DBInfo.username);
ds.setPassword(DBInfo.password);
ds.setInitialSize(2);
ds.setMaxActive(10);
ds.setMaxIdle(10);
ds.setTestWhileIdle(true);
ds.setMinEvictableIdleTimeMillis(60000 * 3);
ds.setTimeBetweenEvictionRunsMillis(10 * 1000);
return ds;
}
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource());
return tm;
}
@Bean
public MemberDao memberDao() {
return new MemberDao(dataSource());
}
}
- DataSource 객체
- @Bean 애노테이션의 destroyMethod = "close" 속성은 커넥션 풀에 보관된 Connection을 닫는 close 메서드를 실행하도록 한다.
- DriverClass, url, username, password를 지정한다.
- setInitialSize(int i)는 커넥션 풀을 초기화할 때 생성할 초기 커넥션 개수를 지정. 기본값 10.
- setMaxActive(int i)는 커넥션 풀에서 가져올 수 있는 최대 커넥션 개수를 지정. 기본값 10.
- setMaxIdle(int i)는 커넥션 풀에 유지할 수 있는 최대 커넥션 개수를 지정.
- setTestWhileIdle(boolean)는 커넥션이 풀에 유휴 상태로 있는 동안에 검사할 지 여부를 결정.
- setMinEvictableIdleTimeMillis(int i)는 커넥션 풀에 유휴 상태로 유지할 최소 시간을 밀리초 단위로 지정.
- setTimeBetweenEvictionRunsMillis(int i)는 커넥션 풀의 유휴 커넥션을 검사할 주기를 밀리초 단위로 지정.
- 커넥션 풀에 커넥션을 요청하면 해당 커넥션은 활성(active) 상태가 되고, 커넥션을 다시 커넥션 풀에 반환하면 유휴(idle) 상태가 된다.
- PlatformTransactionManager 객체
- Spring이 제공하는 @Transactional 애노테이션을 사용하면 트랜잭션 범위를 매우 쉽게 지정할 수 있다.
- @Transactional 애노테이션을 사용하려면 PlatformTransactionManager을 Bean으로 설정해야 한다.
- MemberDao 객체
- DB 연동을 구현한 Bean 객체(DAO)는 DataSource를 주입받아 사용해야 한다.
[ 3 ] MemberDao 구현
Spring을 사용하면 Connection, Statement, ResultSet을 직접 사용하지 않고 JdbcTemplate를 이용하여 쿼리를 편하게 실행할 수 있다. JdbcTemplate을 이용하여 이미 가입된 이메일인지를 확인하기 위한 selectByEmail 메서드와 가입 후 정보 저장을 위한 insert 메서드를 구현하자.
package dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.format.DateTimeFormatter;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import dto.Member;
public class MemberDao {
private JdbcTemplate jdbcTemplate;
public MemberDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public Member selectByEmail(String email) {
List<Member> results = jdbcTemplate.query(
"select * from Member where email=?",
new RowMapper<Member>() {
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member(
rs.getString("email"),
rs.getString("name"),
rs.getString("sex"),
rs.getString("birthdate"),
rs.getString("password"),
rs.getTimestamp("regdate").toLocalDateTime());
member.setId(rs.getLong("id"));
return member;
}
},
email);
return results.isEmpty() ? null : results.get(0);
}
public void insert(Member member) {
// 자동으로 생성된 key값을 구하기 위해 keyHolder 사용
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
PreparedStatement pstmt = con.prepareStatement(
"insert into Member (email, name, sex, birthdate, password, regdate) values (?,?,?,?,?,?)",
new String[] {"id"});
pstmt.setString(1, member.getEmail());
pstmt.setString(2, member.getName());
pstmt.setString(3, member.getSex());
pstmt.setString(4, member.getBirthDate());
pstmt.setString(5, member.getPassword());
pstmt.setTimestamp(6, Timestamp.valueOf(member.getRegdate()));
return pstmt;
}
}, keyHolder);
Number keyValue = keyHolder.getKey();
member.setId(keyValue.longValue());
}
}
- selectByEmail 메서드
- JdbcTemplate는 SQL의 select 쿼리를 실행하기 위한 query() 메서드를 제공한다.
- List <T> query(String sql, RowMapper<T> rowMapper, Object ... args)
- 두 번째 파라미터인 rowMapper는 쿼리 실행 결과를 자바 객체로 변환하는 함수형 인터페이스이다. 그러므로 인자로 익명 클래스나 람다식이 올 수 있다. (지금은 익명 클래스를 넘겨주었으나, 람다식이 더 깔끔하다)
- sql에 있는 인덱스 파라미터(물음표)에 들어갈 값은 세 번째 인자로 넘겨준다. 인덱스 파라미터가 두 개 이상이라면 물음표 개수만큼 해당되는 값을 전달한다.
- 결과가 한 행이므로 queryForObject() 메서드로 대체할 수 있다.
- insert 메서드
- SQL의 insert, update, delete 쿼리를 실행하기 위해서는 update() 메서드를 사용한다.
- int update(String sql, Object ... args)
- PreparedStatement를 사용하기 위해서는 PreparedStatementCreator를 인자로 받는 메서드를 이용하여 직접 PreparedStatement를 생성해야 한다.
- Member table의 id는 auto_increment 설정이 되어 있으므로 행이 추가되면 값이 자동으로 할당된다.
- 쿼리 실행 후 자동으로 생성된 키값을 리턴받기 위해서는 KeyHolder를 사용해야 한다.
[ 4 ] Service 구현
'회원가입'이라는 기능의 로직을 제공하는 서비스를 구현하자. 해당 기능의 로직은 다음과 같다.
(1) 폼에서 전달받은 이메일이 DB에 존재한다면 DuplicateMemberException을 throw한다.
(2) DB에 존재하지 않는다면 memberDao.insert(newMember) 를 통해 DB에 데이터를 추가한다.
package service;
import java.time.LocalDateTime;
import command.RegisterCommand;
import dao.MemberDao;
import dto.Member;
import exception.DuplicateMemberException;
public class RegisterService {
private MemberDao memberDao;
public RegisterService(MemberDao memberDao) {
this.memberDao = memberDao;
}
public Long regist(RegisterCommand registerCommand) {
Member member = memberDao.selectByEmail(registerCommand.getEmail());
if(member != null) {
throw new DuplicateMemberException(registerCommand.getEmail() +"는 이미 가입된 이메일입니다.");
}
Member newMember = new Member(registerCommand.getEmail(), registerCommand.getName(), registerCommand.getSex(), registerCommand.getBirthdate(), registerCommand.getPassword(), LocalDateTime.now());
memberDao.insert(newMember);
return newMember.getId();
}
}
+) Spring은 요청 파라미터의 값을 커맨드(Command) 객체에 담아주는 기능을 제공한다. 요청 파라미터의 값을 전달받을 수 있는 setter 메서드를 포함하는 객체를 커맨드 객체로 사용할 수 있다. 현재 regist 메서드의 파라미터 타입은 RegisterCommand이며 이는 커맨드 객체인데, 아래에서 추가적으로 설명하도록 하겠다.
MemberConfig에 RegisterService를 Bean으로 등록하고, 의존성 주입을 해 준다.
package config;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import dao.MemberDao;
import db.DBInfo;
import service.RegisterService;
@Configuration
@EnableTransactionManagement
public class MemberConfig {
@Bean(destroyMethod = "close")
public DataSource dataSource() {
...
}
@Bean
public PlatformTransactionManager transactionManager() {
...
}
@Bean
public MemberDao memberDao() {
return new MemberDao(dataSource());
}
@Bean
public RegisterService registerService() {
return new RegisterService(memberDao());
}
}
[ 5 ] Controller 구현
기본적인 회원가입 절차는 다음과 같다.
terms.jsp (약관동의) → form.jsp (회원가입 정보 입력) → success.jsp (회원가입 성공 문구 안내)
(1) 약관에 동의해야 회원가입을 할 수 있다. (동의하지 않으면 form.jsp로 이동할 수 없다)
(2) form.jsp에서 회원가입 정보를 입력한다.
(3) 회원가입에 성공하면 success.jsp를 return한다.
package controller;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import command.RegisterCommand;
import service.RegisterService;
@Controller
public class RegisterController {
private RegisterService registerService;
public RegisterController(RegisterService registerService) {
this.registerService = registerService;
}
@GetMapping("/register/terms")
public String handlerterms() {
return "register/terms";
}
@PostMapping("/register/form")
public String handlerForm(HttpServletRequest request) {
String agree = request.getParameter("agree");
if(agree == null || !agree.equals("true")) {
return "register/terms";
}
return "register/form";
}
@GetMapping("/register/form")
public String handlerFormGet() {
return "redirect:/register/terms";
}
@GetMapping("/register/success")
public String handlerSuccessGet() {
return "redirect:/register/terms";
}
@PostMapping("/register/success")
public String handlerSuccess(RegisterCommand registerCommand) {
registerService.regist(registerCommand);
return "register/success";
}
}
- handlerterms() 메서드 : Get요청이 오면 terms.jsp return
- handlerForm(HttpServletRequest request) 메서드
- request 객체로 약관동의 여부를 확인하고, 약관동의를 하지 않았다면 terms.jsp로 돌아간다.
- 약관동의를 했다면 form.jsp를 return
- handlerSuccess(RegisterCommand registerCommand) 메서드
- form.jsp의 form 내부 데이터를 registerCommand 객체로 받아온다.
- registerService.regist(registerCommand); 로 새로운 회원의 정보를 등록한다.
- 가입이 완료되면 success.jsp를 return
- RegisterCommand 객체
package command;
public class RegisterCommand {
private String email;
private String name;
private String sex;
private String birthdate;
private String password;
private String confirmPassword;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getBirthdate() {
return birthdate;
}
public void setBirthdate(String birthdate) {
this.birthdate = birthdate;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getConfirmPassword() {
return confirmPassword;
}
public void setConfirmPassword(String confirmPassword) {
this.confirmPassword = confirmPassword;
}
public boolean isPasswordEqualToConfirmPassword() {
return password.equals(confirmPassword);
}
}
마지막으로 Controller를 ControllerConfig의 Bean으로 등록한다. 이미 Bean으로 등록된 RegisterService는 @Autowired로 의존 자동 주입 받는다.
package config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import controller.RegisterController;
import service.RegisterService;
@Configuration
public class ControllerConfig {
@Autowired
private RegisterService registerService;
@Bean
public RegisterController registerController() {
return new RegisterController(registerService);
}
}
위 예제 실행 결과를 확인한다.
잘 등록되었음을 확인했다!
다음에는 Validator 인터페이스를 구현하여 RegisterCommand 객체를 검증(올바른 이메일, 생년월일 형식인지 등)해 보도록 하자.
❗️DuplicateMemberException와 jsp파일은 github에서 확인할 수 있습니다❗️
'Project > [Spring] 게시판' 카테고리의 다른 글
[Spring] 게시판 만들기 - 로그인 및 로그아웃 구현 (2) | 2023.02.13 |
---|---|
[Spring] 게시판 만들기 - 커맨드 객체 검증 및 에러 코드 지정 (1) | 2023.02.11 |
[Spring] 게시판 만들기 - gradle 설정, Spring MVC 설정, DispatcherServlet 설정 (1) | 2023.02.07 |