2023.08.27 TIL - Spring
1. Spring
Spring은 데이터 통신을 위한 자바 프레임워크이다. 프로젝트 생성 시 스프링을 선택할 경우 main 메소드에 자동으로 스프링이 톰캣 서버를 연동해 데이터 통신을 할 수 있게 해 준다.
데이터 통신을 위한 http 메소드 4가지를 학습했다.
- GET: 리소스 조회
- POST: 요청 데이터 처리, 주로 등록에 사용
- PUT: 리소스를 대체, 해당 리소스가 없으면 생성
- DELETE: 리소스 삭제
Spring에서는 이들을 각각 @GetMapping, @PostMapping, @PutMapping, @DeleteMapping 어노테이션을 통해 쉽게 사용할 수 있게 만들어 준다. 이들은 컨트롤러를 통해 제어되는데, 이 또한 Controller 어노테이션을 활용하면 클래스를 컨트롤러로 활용할 수 있다.
DTO는 Data Transfer Object의 약자로, 데이터 전송 및 이동을 위해 생성되는 객체를 의미하며, Client에서 보내는 데이터를 처리할 때도 사용되지만, 서버의 계층 간 통신에도 사용된다. 일반적으로 Response를 위한 객체는 ResponseDto, Request를 위한 객체는 RequestDto라는 이름으로 많이 사용한다. 또한 이렇게 생성된 DTO들은 클래스의 필드 중 전부를, 혹은 일부분을 포함하는데, 상속은 하지 않고 별개의 클래스로 생성한다.
실습에서는 메모 클래스를 활용한 메모지 구현을 통해 이들을 학습했는데, 그 때 사용한 responseDto, requestDto와 Memo객체는 아래와 같다. 호출 메소드는 별도로 기재하지 않았다.
public class Memo {
private Long id;
private String username;
private String contents;
}
public class MemoRequestDto {
private String username;
private String contents;
}
public class MemoResponseDto {
private Long id;
private String username;
private String contents;
}
id는 내부 저장소에서 자체적으로 설정해 줄 예정이기에 Response에는 id를 함께 출력했으나, Request에는 필요하지 않아 넣지 않았다.
2. DTO를 활용한 Create, Read 구현
내부 저장소는 Map을 활용해 key에는 id, value에는 Memo 클래스를 받도록 구현하였다.
@PostMapping("memos")
public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
// RequestDto 를 Entity 로 변경
Memo memo = new Memo(requestDto);
// Memo Max ID Check (입력하면 id 중복이 안되도록 가장 큰 id에서 1 더한 값을 불러옴
Long maxId = memoList.size() > 0 ? Collections.max(memoList.keySet()) + 1 : 1;
memo.setId(maxId);
// DB 저장
memoList.put(memo.getId(), memo);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(memo);
return memoResponseDto;
}
requestDto를 통해 username과 contents를 request받는다.
이를 Memo Entity로 변경해 주면, id값이 없는 Memo 클래스가 생성된다.
내부 저장소의 데이터 중 가장 큰 id값을 받아온 뒤, 이보다 1 큰 값을 생성한 Memo의 Id로 할당한다.
그렇게 생성된 Memo는 List에 저장하고, 이와 필드값이 동일한 ResponseDto를 선언한 후 반환해 주면 클라이언트에서는 입력한 메모의 데이터를 출력받을 수 있다.
@GetMapping("memos")
public List<MemoResponseDto> getMemos() {
// Map To List
List<MemoResponseDto> responseList = memoList.values().stream()
.map(MemoResponseDto::new).toList();
return responseList;
}
저장한 Map에서 value의 값들의 리스트 집합을 불러온다. 저장되어 있는 value는 Memo이나 출력을 위해 MemoResponseDto의 형태로 매핑한 후, 전체를 반환한다.
3. Update, Delete 구현
@PutMapping("memos/{id}")
public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
// 해당 메모가 DB에 존재하는지 확인
if (memoList.containsKey(id)) {
//메모 가져오기
Memo memo = memoList.get(id);
// 메모 수정
memo.update(requestDto);
return memo.getId();// = id
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
경로 변수를 통해 메모의 id를, requestDTO를 통해 수정할 메모의 텍스트를 받는다.
받아온 id 값을 갖는 메모가 있다면, 받아온 텍스트를 해당 id값의 Memo에 할당하고, 없을 경우 Exception을 throw한다.
@DeleteMapping("/memos/{id}")
public Long deleteMemo(@PathVariable Long id) {
// 해당 메모가 DB에 존재하는지 확인
if (memoList.containsKey(id)) {
memoList.remove(id);
return id;
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
Update와 비슷하게 메모 id를 경로 변수를 통해 불러온다. 이 id 값의 메모가 있다면 해당 메모를 삭제하고, 없다면 동일하게 예외 처리를 한다. 데이터 삭제를 위함이기에 id를 제외하면 받아올 값이 없고, id는 경로 변수로 받았기에 requestDTO는 받아오지 않는다.
4. 데이터베이스와 jdbc
위에서 구현한 Memo 클래스는 실행할 때 Java의 Map 클래스에 저장해 두었다. 문제는, 실행 중인 어플리케이션을 종료하면 지금까지 저장했던 모든 데이터도 함께 날아간다. 그렇기에 실제로는 자료를 데이터베이스에 저장한 후, 이를 호출하는 형태로 구현한다.
현실에는 수많은 데이터베이스 형태가 존재하는데, 자바에서 이들을 직접 연결해 사용하려면 데이터베이스마다 커넥션 방법, request와 response 방법을 모두 설정해 주어야 한다. Java DataBase Connectivity, 줄여서 jdbc는 이를 해결하기 위해 만들어진 데이터베이스에 접근할 수 있도록 java에서 제공하는 API이다.
각 DB 회사들이 자신들의 DB에 맞게 JDBC driver를 제공하면, db 로직 변경 없이 db 변경이 가능하다. 하지만 그럼에도 데이터 연결을 위해서는 수많은 작업들이 필요하다. 커넥션 연결, 해제 및 상태 설정 등의 중복되는 작업들을 대신 처리해 주는 용도로 jdbcTemplate가 등장했다. 데이터 통신 과정에서 jdbc를 활용하여 코드를 변경하였다. 사용한 db는 mysql이다.
아래에는 sql 콘솔 명령어에 대한 설명은 생략한다.
private final JdbcTemplate jdbcTemplate;
public MemoController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
JdbcTemplate를 활용하기 위해 필드를 생성하고, 호출 메소드를 정의한다.
@PostMapping("/memos")
public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// DB 저장
KeyHolder keyHolder = new GeneratedKeyHolder(); // 기본 키를 반환받기 위한 객체
String sql = "INSERT INTO memo (username, contents) VALUES (?, ?)";
jdbcTemplate.update( con -> {
PreparedStatement preparedStatement = con.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, memo.getUsername());
preparedStatement.setString(2, memo.getContents());
return preparedStatement;
},
keyHolder);
// DB Insert 후 받아온 기본키 확인
Long id = keyHolder.getKey().longValue();
memo.setId(id);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(memo);
return memoResponseDto;
}
Create의 경우. 전체적인 구조는 결국 동일하다. 단지 db와 소통하기 위한 부분들이 전부 sql 콘솔 명령어로 변경되었고, 이를 자바에서 받아들이기 위해 jdbcTemplate를 사용하였다.
@GetMapping("/memos")
public List<MemoResponseDto> getMemos() {
// DB 조회
String sql = "SELECT * FROM memo";
return jdbcTemplate.query(sql, new RowMapper<MemoResponseDto>() {
@Override
public MemoResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
// SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
Long id = rs.getLong("id");
String username = rs.getString("username");
String contents = rs.getString("contents");
return new MemoResponseDto(id, username, contents);
}
});
}
Read의 경우.
@PutMapping("/memos/{id}")
public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
// 해당 메모가 DB에 존재하는지 확인
Memo memo = findById(id);
if(memo != null) {
// memo 내용 수정
String sql = "UPDATE memo SET username = ?, contents = ? WHERE id = ?";
jdbcTemplate.update(sql, requestDto.getUsername(), requestDto.getContents(), id);
return id;
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
Update의 경우.
@DeleteMapping("/memos/{id}")
public Long deleteMemo(@PathVariable Long id) {
// 해당 메모가 DB에 존재하는지 확인
Memo memo = findById(id);
if(memo != null) {
// memo 삭제
String sql = "DELETE FROM memo WHERE id = ?";
jdbcTemplate.update(sql, id);
return id;
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
Delete의 경우.
private Memo findById(Long id) {
// DB 조회
String sql = "SELECT * FROM memo WHERE id = ?";
return jdbcTemplate.query(sql, resultSet -> {
if(resultSet.next()) {
Memo memo = new Memo();
memo.setUsername(resultSet.getString("username"));
memo.setContents(resultSet.getString("contents"));
return memo;
} else {
return null;
}
}, id);
}
Update와 Delete의 경우 id를 통해 메모를 불러와야 하는데, 이 부분은 별도의 메소드로 정의해 활용할 수 있도록 하였다.