🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [43]일차
🚀43일차에는 Batch하는 방법에 대해 잠깐 배우고, 트랜잭션에 대해서 배울 수 있었다.
그리고 Spring JDBC와 Spring Data JDBC를 토대로 친구목록 페이지에 대한 프로젝트를 진행해볼 수 있었다.
Batch 업데이트 작업
- JdbcTemplate에서 batchUpdate() 메소드를 통해 Batch 업데이트 작업 가능
- 한번에 여러건을 INSERT하는 기능을 제공하는 것
➡️여러 개의 SQL문을 하나의 배치로 묶어서 실행하는 기법 - 대량의 데이터를 DB에 삽입, 업데이트, 삭제 해야할 경우에 사용
- 장점 : DB 성능 향상 및 네트워크 비용 감소
ex. 대용량 데이터 이전, 로그 처리 작업 등
▶️실습 - DB에 배치작업 수행
// 배치 (Batch) : 여러 건의 SQL문을 한번에 입력하는 작업 수행
@Bean
public CommandLineRunner batchUpdateDemo(JdbcTemplate jdbcTemplate){
return args -> {
List<User> users = Arrays.asList(
new User("kang","kang@exam.com"),
new User("kim","kim@exam.com"),
new User("hong","hong@exam.com"),
new User("lee","lee@exam.com"),
new User("aaa","aaa@exam.com")
);
String sql = "INSERT INTO users(name, email) VALUES(?,?)";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
User user = users.get(i);
ps.setString(1, user.getName());
ps.setString(2, user.getEmail());
}
@Override
public int getBatchSize() {
return users.size();
}
});
};
}
- User user = users.get(i);
다수의 데이터를 넣을 리스트에서 i(인덱스)로 접근해서 가져옴 - ps.setString(1, user.getName());
➡️첫번째 물음표(?)에 대한 바인딩 수행 - ps.setString(2, user.getEmail());
➡️ 두번째 물음표(?)에 대한 바인딩 수행 - return users.size();
➡️넣을 값이 몇개인지를 명시 (=배치작업할 사이즈를 알려주는 메소드)
ex. 다수의 데이터 (5개)이면 배치작업할 사이즈는 5
트랜잭션 Transaction
- 트랜잭션의 중요성 : 여러 작업을 그룹화하여 전체가 성공적으로 완료되거나 실패할떄 모두 롤백되는 원자성을 보장
- Spring에서 선언적 트랜잭션 관리 방법
➡️@Transactional 어노테이션 사용 및 트랜잭션 전파 수준 설정 - TransactionTemplate을 사용하여 보다 세밀한 제어가 필요한 상황에서 트랜잭션을 관리
UserDao 인터페이스
public interface UserDao {
void createAndUpdateUser(String name, String email, String newEmail);
}
UserDaoImpl 구현체
@RequiredArgsConstructor
@Repository
public class UserDaoImpl implements UserDao {
private final JdbcTemplate jdbcTemplate;
public void createAndUpdateUser(String name, String email, String newEmail){
// 1. 값 입력
jdbcTemplate.update("INSERT INTO users(name, email) VALUES(?,?)", name, email);
// 테스트를 위한 예외발생시키기
if(newEmail.contains("error")){
// error라고 newEmail에 들어왔으면
throw new RuntimeException("error");
}
// 2. 값 수정
jdbcTemplate.update("UPDATE users SET email = ? WHERE name = ?", newEmail, name);
}
}
- 쿼리 입력 후 "에러 발생"이 되면 그 이전에 입력된 쿼리도 롤백 되어야함
➡️즉 트랜잭션 처리의 동작 과정 INSERT이 끝난 후
➡️RuntimeException 발생에 에러가 발생했으므로
➡️UPDATE 또한 실행되지 않아야함
@SpringBootApplication
@Bean
public CommandLineRunner demo(UserDao userDao){
return args -> {
userDao.createAndUpdateUser("Conor", "Bradley@premier.com", "Rightback@premier.com");
};
}
▶️실습 - 예외를 발생시켜서 롤백이 수행되는지 확인
userDao.createAndUpdateUser("Conor", "Rightback@premier.com", "error");
- error를 newEmail에 넣어 예외를 발생시키고 테스트 확인
- error로 예외가 발생되었지만 이전에 입력했던
“Conor” “Rightback@premier.com” 이라는 값또한 INSERT 쿼리 수행으로 입력된 모습이다. - 올바른 롤백 수행 시에는 이전의 INSERT 쿼리는 수행되지 않아야 한다.
// 1. 값 입력
jdbcTemplate.update("INSERT INTO users(name, email) VALUES(?,?)", name, email);
if(newEmail.contains("error")){ throw new RuntimeException("error"); }
// 2. 값 수정
jdbcTemplate.update("UPDATE users SET email = ? WHERE name = ?", newEmail, name);
- 예외 발생을 시각화하고자 error를 값 입력과 값 수정 사이에 위치시키면
error발생 시 "1. 값 입력" 부분이 롤백되어야함 - 예를 들어 출금 후 입금하기전에 예외가 발생하면 출금도 모두 롤백되어야하는데
롤백되지 않는 경우에 출금만 되고 입금은 수행되지 않을 것이다. - ➡️해결방법 : 트랜잭션 생성과 처리를 도와주는 @Transactional 어노테이션을 정의해야함
트랜잭션 관리 방법 2가지
1. 선언적 트랜잭션 관리 @Transactional
▶️실습 - 트랜잭션 생성 및 처리 (해결방법) - @Transactional
@Transactional
public void createAndUpdateUser(String name, String email, String newEmail){
// 1. 값 입력
jdbcTemplate.update("INSERT INTO users(name, email) VALUES(?,?)", name, email);
if(newEmail.contains("error")){ throw new RuntimeException("error"); }
// 2. 값 수정
jdbcTemplate.update("UPDATE users SET email = ? WHERE name = ?", newEmail, name);
}
userDao.createAndUpdateUser("Dominic", "Solanke@premier.com", "error");
- @Transactional 어노테이션 처리를 해준 후에는 error를 발생시켜도
- "1. 값 입력" 부분에 넣었던 (“Dominic” “Solanke@premier.com”)의 값이 롤백되어 DB에 적용이 되지 않을 것이다.
- 즉 예외발생 후 그 이전의 수행한 작업도 성공적으로 롤백된 것
▶️실습 - 트랜잭션 분리
- 기존에 createAndUpateUser()메소드에서 한번에 입력, 수정을 테스트해봤다면
- 메소드가 각자의 기능을 하도록 분리해줄 수 있다.
@Transactional
public void addUser(String name, String email){
jdbcTemplate.update("INSERT INTO users(name, email) VALUES(?,?)", name, email);
}
@Transactional
public void updateUser(String name, String newEmail){
jdbcTemplate.update("UPDATE users SET email = ? WHERE name = ?", newEmail, name);
}
- 2개의 메소드로 분리한 후 @Transactional 어노테이션 정의할 수 있다.
public interface UserDao {
void createAndUpdateUser(String name, String email, String newEmail);
void addUser(String name, String email);
void updateUser(String name, String newEmail);
}
- 분리된 addUser(), updateUser() 메소드는 인터페이스에서도 정의되어있어야하므로 추가
UserService 클래스 - 메소드의 기능을 실제로 동작하도록 구현
@RequiredArgsConstructor
@Service
public class UserService {
private final UserDao userDao;
private final TransactionTemplate transactionTemplate;
public void createAndUpdateUser(String name, String email, String newEmail){
userDao.addUser(name, email);
if(newEmail.contains("error")){
throw new RuntimeException("error");
}
userDao.updateUser(name, newEmail);
}
}
- 실제로 서비스가 동작되면서 수행되어야하므로 서비스에 실제 기능할 메소드를 정의
- createAndUpdateUser(...)
@Service가 이 메소드 안에서 UserDao의 각각의 메소드들 void addUser(…); void updateUser(…); 를 정의
UserService의 createAndUpdateUser()에 @Transactional 테스트
// 1. @Transactional 없이 테스트
public void createAndUpdateUser(String name, String email, String newEmail){...}
// 2. @Transactional 붙여서 테스트
@Transactional
public void createAndUpdateUser(String name, String email, String newEmail){...}
- @Transactional 의 여부에 따른 테스트를 진행가능
- ➡️@Transactional이 붙으면 롤백이 성공적으로 수행
➡️@Transactional이 붙어있지 않으면 updateUser()는 실행되지 않지만 addUser()는 실행됨 (롤백이 되지 않음)
@Transactional을 붙여 롤백 테스트
@Bean
public CommandLineRunner demo(UserService userService){
return args -> {
//userDao.createAndUpdateUser("Dominic", "Solanke@premier.com", "error");
userService.createAndUpdateUser("Owen", "Beck@premier.com", "error");
};
}
- 기존에 userDao에서 정의된 메소드를 가지고 사용하던 것을 userService를 통해서
createAndUpdateUser()메소드를 호출하는 것으로 수정 가능 - error가 포함되어있으므로 실제 데이터베이스에는 값이 들어가지 않음 (롤백 성공)
❓userDao.createAndUpdateUser() 와 userService.createAndUpdateUser()의 차이점
➡️UserDao의 @Repository를 바로 사용하지 않고
UserService의 @Service를 통해 실제 비즈니스 로직을 관리해보는 경우로 리팩토링한것
2. 프로그래매틱 트랜잭션 관리 (TransactionTemplate)
▶️실습 - transactionTemplate.execute() 사용
- 선언적 트랜잭션 관리 방법인 @Transactional 어노테이션 사용을 하지 않는 예시
- TransactionTemplate을 사용하여 명시적으로 트랜잭션을 생성하고 관리함으로써 대체 가능
public void createAndUpdateUser2(String name, String email, String newEmail){
transactionTemplate.execute(status -> {
userDao.addUser(name, email);
if(newEmail.contains("error")){
// 문제가 발생했을때 롤백시키기 위하여
status.setRollbackOnly();
throw new RuntimeException("error");
}
userDao.updateUser(name, newEmail);
return null;
});
}
❓선언적 트랜잭션 관리 (@Transactional)와
프로그래매틱 트랜잭션 관리(TransactionTemplate)의 차이점
➡️선언적 트랜잭션 관리 (@Transactional) 사용
@Transactional
public void createAndUpdateUser(String name, String email, String newEmail){
userDao.addUser(name, email);
if(newEmail.contains("error")){
throw new RuntimeException("error");
}
userDao.updateUser(name, newEmail);
}
- @Transactional을 사용하여 트랜잭션을 관리
1. 메서드 실행이 시작될 때 트랜잭션이 자동 시작되고, 예외 발생 시 자동 롤백
➡️RuntimeException 또는 Error가 발생하면 자동 롤백된다는 것 - 특징 : 기본적으로는 롤백되지 않음 (rollbackFor 지정 가능)
➡️메소드 내부에서 트랜잭션 제어 불가능 (프록시 기반으로 동작하기때문)
➡️프로그래매틱 트랜잭션 관리 (TransactionTemplate) 사용
public void createAndUpdateUser2(String name, String email, String newEmail){
transactionTemplate.execute(status -> {
userDao.addUser(name, email);
if(newEmail.contains("error")){
// 문제가 발생했을때 롤백시키기 위하여
status.setRollbackOnly();
throw new RuntimeException("error");
}
userDao.updateUser(name, newEmail);
return null;
});
}
- TransactionTemplate을 사용으로 명시적으로 트랜잭션을 관리
- 트랜잭션이 실행될 블록을 execute() 메서드로 감싸서 수행
- status.setRollbackOnly();
➡️setRollbackOnly() 메소드 호출 시 현재 트랜잭션이 롤백 상태로 표시
➡️이후 execute()가 종료될 때 롤백됨. - 예외 발생 시 트랜잭션이 자동으로 롤백됨
ex. RuntimeException 발생 시 롤백 - 특징 : 강제 롤백 가능 (status.setRollbackOnly())
➡️메소드 내부에서 트랜잭션 제어 가능
정리하자면
선언적 트랜잭션 관리 (@Transactioonal) :
➡️어노테이션을 사용하여 메소드 전체를 트랜잭션으로 처리, 에러 발생 시 자동으로 롤백
프로그래매틱 트랜잭션 관리 (TransactionTemplate) :
➡️트랜잭션을 프로그래밍 방식으로 관리, 에러 발생 시 트랜잭션을 명시적으로 롤백 설정
ResultSetExtractor
- 관련하여 RowMapper가 있다. ResultSetExtractor는 RowMapper보다 더 복잡한 쿼리를 다룬다.
➡️좀 더 복잡한 형태의 값을 채워야할때 사용 - RowMapper<T>
➡️Spring JDBC (Spring Data JDBC 포함)에서
ResultSet의 각 행(row)을 Java 객체로 변환하는 기능을 하는 인터페이스
즉, 쿼리 결과를 원하는 객체 형태로 변환할 때 사용
Reply 클래스
- 댓글 클래스
public class Reply {
private int id;
private String content;
public Reply(int id, String content) {
this.id = id;
this.content = content;
}
@Override
public String toString() {
return "Reply{" + "id=" + id + ", content='" + content + "'}";
}
}
Board 클래스
- 게시물 클래스 (제목, 내용 필드 포함)
public class Board {
private int id;
private String title;
private String content;
private List<Reply> replies = new ArrayList<>();
public Board(int id, String title, String content) {
this.id = id;
this.title = title;
this.content = content;
}
public void addReply(Reply reply) {
replies.add(reply);
}
@Override
public String toString() {
return "Board{" + "id=" + id + ", title='" + title + "', content='" + content + "', replies=" + replies + '}';
}
}
ResultSetExtractor 구현
public class BoardResultSetExtractor implements ResultSetExtractor<Map<Integer, Board>> {
@Override
public Map<Integer, Board> extractData(ResultSet rs) throws SQLException {
Map<Integer, Board> boardMap = new HashMap<>();
while (rs.next()) {
int boardId = rs.getInt("board_id");
// 기존 Board 객체 확인 (이미 저장된 경우 가져옴)
Board board = boardMap.get(boardId);
if (board == null) {
board = new Board(
boardId,
rs.getString("title"),
rs.getString("content")
);
boardMap.put(boardId, board);
}
// 댓글이 존재하면 추가
int replyId = rs.getInt("reply_id");
if (replyId != 0) { // reply_id가 0이면 댓글이 없는 게시글
Reply reply = new Reply(replyId, rs.getString("reply_content"));
board.addReply(reply);
}
}
return boardMap;
}
}
- 유저가 여러개의 포스트를 가지고 있는 경우
➡️유저 1명 당 글을 여러개 쓸 수 있음
➡️ex. User(1) : Post(n)의 관계 (1:n관계) - Map<Integer, Board> boardMap = new HashMap<>();
board_id를 키로 하고 Board 객체를 값으로 저장하는 Map을 생성
Board 객체의 중복 생성을 방지하기 위해 사용 - while (rs.next()) { ... }
SQL 조회 결과(ResultSet)를 한 행씩 처리
CommandLineRunner
@Bean
public CommandLineRunner demoExtractor(JdbcTemplate jdbcTemplate) {
return args -> {
// ResultSetExtractor example
String sql = "SELECT id, name, email FROM users";
User user = jdbcTemplate.query(sql, new UserResultSetExtractor());
System.out.println(user);
};
}
- User user = jdbcTemplate.query(sql, new UserResultSetExtractor());
new UserResultSetExtractor() 두번째 인자부분에 기존에는 rowMapper 객체가 위치 - 간단하게 테이블 하나에 테이블 하나 처럼 채울때는 rowMapper로 채워주면되지만
Board : Reply (1:N) 와 같은 관계의 테이블이 있는 경우 (복잡한 구조) ResultSetExtractor를 사용한다는 것 - 실행 흐름 :
1. CommandLineRunner 실행되며 SQL 쿼리 실행
2. jdbcTemplate.query(sql, new BoardResultSetExtractor()) 호출
3. ResultSetExtractor로 데이터를 Map<Integer, Board>로 변환
4. boardMap.values().forEach(System.out::println); ➡️변환된 데이터 출력
5. 게시글(Board)과 해당 댓글(Reply)이 함께 출력되도록 테스트 가능
❓ ResultSetExtractor vs RowMapper의 차이점
➡️RowMapper : 각 행(Row)을 독립적인 객체로 변환할 때 사용
➡️ResultSetExtractor : 복잡한 관계(1:N, N:M)를 가진 데이터 구조를 변환할 때 유리
즉 Board(1) : Reply(N) 같은 부모-자식 관계를 매핑할 때는 ResultSetExtractor 사용이 적합
RowMapper대신 ResultSetExtractor 사용의 장점
➡️객체 재사용으로 메모리 효율적 관리 가능
➡️여러 객체를 한 쿼리로 매핑 가능
SQL 관리
- 외부 SQL파일 별도로 관리한다는 의미
➡️외부 SQL파일을 사용하여 데이터베이스 쿼리를 따로 분리하여 관리하는 방법
➡️코드의 가독성과 유지보수성을 향상시키는 효과적인 방법 - 쿼리를 클래스에서 가지고 있는것이 아닌 별도로 관리
- 장점 :
➡️개발자는 비즈니스 로직만 관리하고 SQL 변경은 DBA or SQL 전문가가 관리
➡️SQL 변경 시 코드 수정 없이 배포 가능
➡️다양한 DB 지원 가능
➡️코드 가독성 향상
▶️실습 - 외부 관리 방법 (queries.sql)
-- SQL to insert a user
INSERT_USER=INSERT INTO users (name, email) VALUES (:name, :email);
-- SQL to fetch all users
GET_ALL_USERS=SELECT id, name, email FROM users;
-- SQL to update a user's email
UPDATE_USER_EMAIL=UPDATE users SET email = :email WHERE name = :name;
-- SQL to delete a user
DELETE_USER=DELETE FROM users WHERE name = :name;
- 외부 SQL 파일을 Spring 에서 불러오기 위해서는 (=로드하기 위해서는) Resource 객체를 사용 가능
▶️실습 - Resource객체로 외부 SQL파일 불러오기
- @Configuration 어노테이션을 사용하는 SqlConfig 클래스
@Configuration
public class SqlConfig {
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
@Bean
public Properties sqlQueries() throws IOException {
Resource resource = new ClassPathResource("sql/queries.sql");
Properties properties = new Properties();
properties.load(resource.getInputStream());
return properties;
}
}
Repository의 UserDao클래스(@Repository)
@Repository
public class UserDao {
@Autowired
private NamedParameterJdbcTemplate jdbcTemplate;
@Autowired
private Properties sqlQueries;
public void insertUser(User user) {
String sql = sqlQueries.getProperty("INSERT_USER");
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("name", user.getName())
.addValue("email", user.getEmail());
jdbcTemplate.update(sql, params);
}
}
- getProperty(”INSERT_USER”)
➡️queries.sql 파일의 INSERT_USER=INSERT INTO users (name, email) VALUES (:name, :email);
의 "INSERT_USER" 키(Key)와 일치해야함
친구목록 프로젝트
샘플데이터 테이블 구성
-- friendApp 게시판 만들기 위한 테이블 구성
CREATE TABLE friend (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255)
);
INSERT INTO friend (name, email) VALUES
('김민수', 'minsu.kim@example.com'),
('이하은', 'haeun.lee@example.com'),
('박서준', 'seojun.park@example.com'),
('최지우', 'jiwoo.choi@example.com'),
('정다현', 'dahyun.jung@example.com'),
('손예진', 'yejin.son@example.com'),
('노준호', 'junho.noh@example.com'),
('윤서아', 'seoah.yoon@example.com'),
('임태현', 'taehyun.lim@example.com'),
('한지안', 'jian.han@example.com'),
('조유리', 'yuri.jo@example.com');
domain/Friend클래스 구현
- @Table의 SpringData JDBC가 아닌 Spring JDBC사용
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Friend {
private Long id;
private String name;
private String email;
}
repository/FriendRepository 인터페이스 구현
public interface FriendRepository extends CrudRepository<Friend, Long>,
PagingAndSortingRepository<Friend, Long> {
}
- PagingAndSortingRepository
➡️정렬관련 기능 제공
➡️equals()메소드가 오버라이딩 되어야함 (정렬 기준이 필요하므로)
domain/Friend 클래스 수정
- @EqualsAndHashCode 어노테이션을 추가하여 정렬 기준을 정의
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@EqualsAndHashCode
public class Friend {
@Id
private Long id;
private String name;
private String email;
}
controller/FriendController 클래스 구현
@Controller
@RequestMapping("/friends")
public class FriendController {
@GetMapping("/list")
public String list(){
// 해야할 일
return "friends/list";
}
}
- @RequestMapping("/friends")
➡️리스트를 보여달라고 요청했을때 url이 어떻게 들어와야될지 설정 (/friends)
한건이 아닌 여러건이 들어올 수 있으므로 기본 url구조 명칭으로 (friends)을 설정 - list()메소드를 통해 친구목록을 리스트로 불러오기 위해서는
[컨트롤러 - 서비스 - 레파지토리] 구조에 따라
Service를 통해서 Service가 Repository로 가서 데이터를 얻어와야함
service/FriendService 클래스 구현
@Service
@RequiredArgsConstructor
public class FriendService {
private final FriendRepository friendRepository;
@Transactional(readOnly = true)
public Iterable<Friend> findAllFriend(){
return friendRepository.findAll();
}
}
- findAll() 메소드는 반환값이 Iterable타입
- readOnly 옵션은 생략해도되겠지만 읽기전용임을 명시하는 것을 권장
controller/FriendController 클래스
@RequiredArgsConstructor
public class FriendController {
private final FriendService friendService;
//...
- private final FriendService friendService
➡️@FriendService를 연결하고, final이므로 @RequiredArgsConstructor를 추가하여 final필드를 사용하도록 함
@GetMapping("/list")
public String list(Model model){
// 해야할 일
model.addAttribute("friends", friendService.findAllFriend());
return "friends/list";
}
- Model을 받아서 addAttribute()를 통해 Repository에서 구현했던 findAllFriend()메소드를 불러오도록 구현
(리스트를 뿌려주기위함)
templates/friends/list.html
- 컨트롤러(controller/FriendController)에서 return "friends/list"; 으로 반환하기 때문
<table>
<thead>
<tr>
<th>아이디</th>
<th>이름</th>
<th>이메일</th>
</tr>
</thead>
<tbody>
<tr th:each="friend:${friends}">
<td th:text="${friend.id}"></td>
<td th:text="${friend.name}"></td>
<td th:text="${friend.email}"></td>
</tr>
</tbody>
</table>
- <tbody> 부분에서 <td>가 3개 들어올 수 있음 (id, name, email)
- (id, name, email)이 값으로 출력되어야함
➡️${friends}를 통해 리스트를 가져와서 순회하고 그 값을 friend로 받을 것
▶️실습 - 리스트가 가져와지지 않을 경우의 테스트
@SpringBootApplication
@Bean
public CommandLineRunner run(FriendRepository repository){
return args -> {
repository.findAll().forEach(System.out::println);
};
}
- 로그창에 결과가 잘 가져와짐
➡️데이터베이스 연결이 원활한것 (=Dao가 잘 동작하고 있는 것) - findAll()
➡️Spring Data JDBC에서 제공해주는 메소드
▶️실습 - 친구추가 구현
- 컨트롤러에서 “친구추가 요청”이 오면
- 친구추가 form 보여주기
- 추가된 친구 데이터 DB에 넣기 를 모두 수행해야함
➡️요청은 1개이지만 결국 2가지 작업 수행 - 1개의 /add (URL)로 GET, POST 방식으로 2가지 작업 구현 가능
controller/FriendController 클래스 - 1. @GetMapping 추가
// 친구추가 폼 보여주기
@GetMapping("/add")
public String addForm(Model model){
model.addAttribute("friend", new Friend());
return "friends/form";
}
- @GetMapping("/add")
➡️친구추가 폼 보여주기
friends/form.html 구현
<body>
<h1>☺️친구 등록</h1>
<form th:action="@{/friends/add}" method="post" th:object="${friend}">
<label for="name">이름 : </label>
<input type="text" id="name" th:field="*{name}" required/>
<label for="email">이메일 : </label>
<input type="text" id="email" th:field="*{email}" required/>
<button type="submit">친구등록</button>
</form>
</body>
- method="post"
➡️값 제출 (submit)을 post방식으로 보내줌 - required ➡️필수로 입력되어야한다는 것
controller/FriendController 클래스 - 2. @PostMapping 추가
// 친구를 저장 - 저장할 정보를 가져와야함
@PostMapping("/add")
public String addFriend(){
// Service의 친구저장 부분 적용
//...
return "redirect:/friends/list";
}
- POST방식으로 DB에 친구 데이터를 저장하는 것 구현
- return "redirect:/friends/list";
➡️친구 저장후에는 redirect를 통해 리스트를 다시 보여주도록 구현
service/FriendService 클래스 - 저장 메소드 추가
public Friend saveFriend(Friend friend){
return friendRepository.save(friend);
}
- public Friend saveFriend(…)
➡️[친구등록] 기능 이후 저장 후 id값이 새로생겨야하므로 Friend 를 반환타입으로 보낼 수 있음 - return friendRepository.save(friend);
➡️JdbcTemplate 사용 시 무조건 새로운 id를 발급받아 저장하지만
Spring Data JDBC는 save()가 반드시 INSERT만 실행되는 것은 아님
➡️만약 사용자가 보낸 friend객체 안에 id값이 이미 존재한다면 수정을 자동으로 수행
(쿼리로 UPDATE를 하지않아도 내부적으로 UPDATE 쿼리를 수행하게되는 것)
➡️id값이 없다면 내부적으로 INSERT쿼리를 처리하는 것 - 즉 save()는 입력 뿐 아니라 수정에도 쓰일 수 있음
- (Friend friend)
➡️Friend에 값을 담아서 올 것이므로 friend에 저장하면됨
▶️실습 - 저장 메소드 분리 (addFriend(), updateFriend())
public Friend addFriend(Friend friend){ // Friend 반환 타입 : 친구등록 저장 후 id값이 새로생겨야함
return friendRepository.save(friend);
}
public Friend updateFriend(Friend friend){ // Friend 반환 타입 : 친구등록 저장 후 id값이 새로생겨야함
return friendRepository.save(friend);
}
- 메소드 분리의 이유 : 친구가 추가 시 조건(이름이 6자 이상은 안된다 등)이 있을 수 있기때문
- 비즈니스 측면에서는 나눠서 사용하는 것이 나을 수도 있다는 것
- 반면 나눌 필요 없는 경우 save()하나만으로 입력, 수정을 함께 구현하는 것이 나을 수 있음
controller/FriendController 클래스 - 저장 메소드 분리
th:object="${friend}"
- form.html에서 저장했던 object의 키 값에 따라
// 친구를 저장 - 저장할 정보를 가져와야함
@PostMapping("/add")
public String addFriend(@ModelAttribute Friend friend){
// 여기서는 Service의 친구저장하는 메소드를 사용하면됨
friendService.addFriend(friend);
return "redirect:/friends/list";
}
- @ModelAttribute 로 Friend friend를 가져와서 friendService의 addFriend()메소드를 통해
save()를 수행하도록 friend를 보내줄 수 있음
templates/friends/list.html
<a th:href="@{/friends/add}">친구등록</a>
- 친구등록은 친구목록 페이지에서 바로 친구등록 페이지로 갈 수 있도록 하이퍼링크를 걸어줄 수도 있음
- 값이 잘 저장되고, 친구등록 페이지도 잘 동작하는 것을 확인 가능
▶️실습 - 상세페이지 구현
- 이름에 하이퍼링크를 달아서 해당 친구의 상세페이지로 이동
- 상세페이지에는 이름과 이메일 출력하고 List, Edit, Delete 버튼이 수행되도록 구현
list.html - friend.name 수정
<td>
<a th:href="@{/friends/{id}(id=${friend.id})}" th:text="${friend.name}"></a>
</td>
- id에 따라 URL을 찾아가도록 구현
- 값으로 들어가므로 {id}) 그 id값은 <td th:text="${friend.id}">를 바인딩할 수 있도록 구현
controller/FriendController - 상세페이지 기능 추가
// 친구의 이름을 누르면 상세페이지로 이동하도록 구현
@GetMapping("/{id}")
public String detailFriend(@PathVariable(name="id") Long id, Model model){
// Service로부터 id에 해당하는 친구정보를 가져와야함
//...
return "friends/detail";
}
- @GetMapping("/{id}")
➡️url은 {id}로 값을 매핑해서 가져옴 - return "friends/detail";
➡️상세페이지 (detail.html)로 이동하게끔 구현 - @PathVariable(name="id") Long id, Model model
➡️URL에서 값을 꺼내기위해 @PathVariable사용하여 Long타입으로 값을 가져옴 (id)
➡️그 후 Model로 정보를 넘겨보내야하므로 Model 선언
service/FriendService - id를 받아 친구를 찾은 후 Friend로 반환해주는 메소드 구현
// Long타입의 id값을 받아 Friend로 반환해주는 메소드를 구현 (상세페이지 관련 메소드)
@Transactional(readOnly = true)
public Friend findFriendById(Long id){
return friendRepository.findById(id).orElse(null);
}
- orElse(null);
➡️예외발생시 null을 넘기는데 그다지 권장되지는 않는 방법
controller/FriendController - Service에서 구현한 findFriendById() 메소드 추가
// 친구의 이름을 누르면 상세페이지로 이동하도록 구현
@GetMapping("/{id}")
public String detailFriend(@PathVariable(name="id") Long id, Model model){ // URL에서 값을 꺼내기위해 @PathVariable사용하여 가져오고, Long타입으로 받음, 그 후 Model로 정보를 넘겨보내야하므로 Model 선언
// Service로부터 id에 해당하는 친구정보를 가져와야함
model.addAttribute("friend", friendService.findFriendById(id));
return "friends/detail";
}
detail.html
<body>
<h2 th:text="${friend.name}"></h2>
<p th:text="${friend.email}"></p>
<a href="@{/friends/list}">친구목록</a>
<a>수정</a>
<a>삭제</a>
</body>
- friend.name, friend.email을 통해서 값을 가져올 수 있음
🚀실습 - 친구 수정 구현
controller/FriendController 클래스 - 친구 수정 기능 추가
// 친구수정 폼 보여주기
@GetMapping("/update/{id}")
public String updateForm(@PathVariable(name="id") Long id, Model model){
model.addAttribute("friend", friendService.findFriendById(id));
return "friends/updateForm";
}
// 친구를 수정 - 수정할 정보를 가져와야함
@PostMapping("/update/{id}")
public String updateFriend(@PathVariable(name="id") Long id, @ModelAttribute Friend friend){
// 여기서는 Service의 친구수정하는 메소드를 사용하면됨
friend.setId(id);
friendService.updateFriend(friend);
return "redirect:/friends/list";
}
- @PathVariable(name="id") Long id
➡️URL에서 친구의 ID를 추출하여 id 변수에 저장 - friendService.findFriendById(id)
➡️친구 서비스에서 ID에 해당하는 친구 정보를 조회 - model.addAttribute("friend", ...)
➡️조회한 친구 정보를 모델에 추가하여 뷰에서 사용 가능 - @ModelAttribute Friend friend
➡️폼에서 입력된 친구 정보를 Friend 객체로 바인딩 - friend.setId(id)
➡️수정할 친구 객체에 ID를 설정
데이터베이스에서 해당 친구를 업데이트하기 위해 필요 - friendService.updateFriend(friend)
➡️친구 서비스의 메서드를 호출하여 친구 정보를 업데이트
updateForm.html
<form th:action="@{/friends/update/{id}(id=${friend.id})}" method="post" th:object="${friend}">
<label for="name">이름 : </label>
<input type="text" id="name" th:field="*{name}" required/> <!--required : 필수로 입력되어야한다는 것-->
<label for="email">이메일 : </label>
<input type="text" id="email" th:field="*{email}" required/> <!--required : 필수로 입력되어야한다는 것-->
<button type="submit">친구수정</button>
</form>
- th:object="${friend}"
➡️폼에서 사용할 객체를 지정 - th:field="*{name}"
friend 객체의 name 속성과 바인딩
detail.html
<h2 th:text="${friend.name}"></h2>
<p th:text="${friend.email}"></p>
<a th:href="@{/friends/list}">친구목록</a>
<a th:href="@{/friends/update/{id}(id=${friend.id})}">수정</a>
<a>삭제</a>
실행흐름 정리
1.사용자가 친구 목록에서 수정할 친구 선택
2. 선택한 친구의 ID를 기반으로 수정 폼을 요청
3. 수정 폼에서 친구의 정보를 수정하고 제출
4. 제출된 정보는 서버에서 처리되어 친구 정보 업데이트
5. 수정이 완료되면 친구 목록 페이지로 리다이렉트
❓@ModelAttribute와 @PathVariable 차이점
@ModelAttribute | HTML form에서 데이터를 객체로 바인딩할 때 사용 |
@PathVariable | URL 경로에서 변수를 추출할 때 사용 |
➡️URL로 이동할 때는 @PathVariable을 사용
➡️form에서 데이터를 받을 때는 @ModelAttribute를 사용
🚀실습 - 친구 삭제 구현
controller/FriendController
// 친구 삭제
@GetMapping("/delete/{id}")
public String delete(@PathVariable("id") Long id){
// 실제 삭제작업이 일어나려면 Service가 필요할 것이니
// Service에 삭제하는 메소드를 구현해놓고 가져와야함
friendService.deleteFriendById(id);
return "redirect:/friends/list";
}
service/FriendService
// 삭제 메소드 구현
@Transactional
public void deleteFriendById(Long id){
friendRepository.deleteById(id);
}
detail.html
<a th:href="@{/friends/delete/{id}(id=-${friend.id})}">삭제</a>
- 삭제 링크 달기
🚀 이전 회고로부터 @PathVariable, @ModelAttribute 등 사용에 대해 배웠지만 실제 기능에서 활용하고자하니
어려운 점이 많았다.
뷰를 Controller에 연결하는 것과 domain, service, repository, controller 등으로 아키텍처를 나눠 관리하는 것,
이들의 메소드를 연결하는 것 또한 익숙해져야할 필요성을 느꼈다.
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_46일차_"JPA" (0) | 2025.02.12 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_44일차_"페이징 처리" (0) | 2025.02.10 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_42일차_"SQL 기반 페이징 기법" (0) | 2025.02.06 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_42일차_"Spring Data JDBC" (1) | 2025.02.06 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_41일차_"람다식 / 스트림 API" (0) | 2025.02.05 |