1. Entity (Memo)
- 메모 데이터를 저장하는 도메인 객체
- @Getter, @Setter, @AllArgsConstructor를 사용하여 자동으로 Getter 및 생성자 생성
- update() 메서드를 통해 제목과 내용을 수정 가능
코드 예시
1. Layered Architecture 버전
더보기
package com.example.springbasiclayered.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@AllArgsConstructor
public class Memo {
@Setter
private Long id;
private String title;
private String contents;
public Memo(String title, String contents) {
this.title = title;
this.contents = contents;
}
public void update(String title, String contents) {
this.title = title;
this.contents = contents;
}
public void updateTitle(String title) {
this.title = title;
}
}
2. Database Version
더보기
package com.example.springbasicdatabase.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class Memo {
private Long id;
private String title;
private String contents;
public Memo(String title, String contents) {
this.title = title;
this.contents = contents;
}
public void update(String title, String contents) {
this.title = title;
this.contents = contents;
}
public void update(String title) {
this.title = title;
}
}
2. DTO (Data Transfer Object)역할
- 클라이언트가 메모 생성 또는 수정을 요청할 때 사용하는 데이터 객체
MemoRequsteDto
1. Layered Architecture 버전
더보기
package com.example.springbasiclayered.dto;
import lombok.Getter;
/**
* Memo 요청 DTO
*/
@Getter
public class MemoRequestDto {
/**
* 제목
*/
private String title;
/**
* 내용
*/
private String contents;
}
2. Database Version
더보기
package com.example.springbasicdatabase.dto;
import lombok.Getter;
/**
* Memo 요청 DTO
*/
@Getter
public class MemoRequestDto {
/**
* 제목
*/
private String title;
/**
* 내용
*/
private String contents;
}
MemoResponseDto
- 서버에서 클라이언트로 메모 데이터를 응답할 때 사용하는 데이터 객체
1. Layered Architecture 버전
더보기
package com.example.springbasiclayered.dto;
import com.example.springbasiclayered.entity.Memo;
import lombok.Getter;
/**
* Memo 응답 DTO
*/
@Getter
public class MemoResponseDto {
/**
* 식별자
*/
private Long id;
/**
* 제목
*/
private String title;
/**
* 내용
*/
private String contents;
public MemoResponseDto(Memo memo) {
this.id = memo.getId();
this.title = memo.getTitle();
this.contents = memo.getContents();
}
}
2. Database Version
더보기
package com.example.springbasicdatabase.dto;
import com.example.springbasicdatabase.entity.Memo;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Memo 응답 DTO
*/
@Getter
@AllArgsConstructor
public class MemoResponseDto {
/**
* 식별자
*/
private Long id;
/**
* 제목
*/
private String title;
/**
* 내용
*/
private String contents;
public MemoResponseDto(Memo memo) {
this.id = memo.getId();
this.title = memo.getTitle();
this.contents = memo.getContents();
}
}
3. Repository Layer
- DB와 직접적으로 데이터를 주고받는 역할
- 메모를 저장, 조회, 수정, 삭제하는 메서드를 포함
Repository 인터페이스 동일
1. Layered Architecture 버전
더보기
package com.example.springbasiclayered.repository;
import com.example.springbasiclayered.dto.MemoResponseDto;
import com.example.springbasiclayered.entity.Memo;
import org.springframework.stereotype.Repository;
import java.util.*;
/**
* Annotation @Repository는 @Component와 같다, Spring Bean으로 등록한다는 뜻.
* Spring Bean으로 등록되면 다른 클래스에서 주입하여 사용할 수 있다.
* 명시적으로 Repository Layer 라는것을 나타낸다.
* DB와 상호작용하여 데이터를 CRUD하는 작업을 수행한다.
*/
@Repository
public class MemoRepositoryImpl implements MemoRepository {
private final Map<Long, Memo> memoList = new HashMap<>();
@Override
public Memo saveMemo(Memo memo) {
// memo 식별자 자동 생성
Long memoId = memoList.isEmpty() ? 1 : Collections.max(memoList.keySet()) + 1;
memo.setId(memoId);
memoList.put(memoId, memo);
return memo;
}
@Override
public List<MemoResponseDto> findAllMemos() {
// init List
List<MemoResponseDto> allMemos = new ArrayList<>();
// HashMap<Memo> -> List<MemoResponseDto>
for (Memo memo : memoList.values()) {
MemoResponseDto responseDto = new MemoResponseDto(memo);
allMemos.add(responseDto);
}
return allMemos;
}
@Override
public Memo findMemoById(Long id) {
return memoList.get(id);
}
@Override
public void deleteMemo(Long id) {
memoList.remove(id);
}
}
2. Database Version
더보기
package com.example.springbasicdatabase.repository;
import com.example.springbasicdatabase.dto.MemoResponseDto;
import com.example.springbasicdatabase.entity.Memo;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Repository
public class JdbcTemplateMemoRepository implements MemoRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateMemoRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public MemoResponseDto saveMemo(Memo memo) {
// INSERT Query를 직접 작성하지 않아도 된다.
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("memo").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("title", memo.getTitle());
parameters.put("contents", memo.getContents());
// 저장 후 생성된 key값을 Number 타입으로 반환하는 메서드
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
return new MemoResponseDto(key.longValue(), memo.getTitle(), memo.getContents());
}
@Override
public List<MemoResponseDto> findAllMemos() {
return jdbcTemplate.query("select * from memo", memoRowMapper());
}
@Override
public Optional<Memo> findMemoById(Long id) {
List<Memo> result = jdbcTemplate.query("select * from memo where id = ?", memoRowMapperV2(), id);
return result.stream().findAny();
}
@Override
public int updateMemo(Long id, String title, String contents) {
// 쿼리의 영향을 받은 row 수를 int로 반환한다.
return jdbcTemplate.update("update memo set title = ?, contents = ? where id = ?", title, contents, id);
}
@Override
public int updateTitle(Long id, String title) {
return jdbcTemplate.update("update memo set title = ? where id = ?", title, id);
}
@Override
public int deleteMemo(Long id) {
return jdbcTemplate.update("delete from memo where id = ?", id);
}
private RowMapper<MemoResponseDto> memoRowMapper() {
return new RowMapper<MemoResponseDto>() {
@Override
public MemoResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
return new MemoResponseDto(
rs.getLong("id"),
rs.getString("title"),
rs.getString("contents")
);
}
};
}
private RowMapper<Memo> memoRowMapperV2() {
return new RowMapper<Memo>() {
@Override
public Memo mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Memo(
rs.getLong("id"),
rs.getString("title"),
rs.getString("contents")
);
}
};
}
}
4. Service Layer
- 비즈니스 로직을 처리하는 계층
- 데이터 검증 및 예외 처리 수행
- Repository를 활용하여 데이터 조작 수행
MemoService 인터페이스 동일
1. Layered Architecture 버전
더보기
package com.example.springbasiclayered.service;
import com.example.springbasiclayered.dto.MemoRequestDto;
import com.example.springbasiclayered.dto.MemoResponseDto;
import com.example.springbasiclayered.entity.Memo;
import com.example.springbasiclayered.repository.MemoRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Annotation @Service는 @Component와 같다, Spring Bean으로 등록한다는 뜻.
* Spring Bean으로 등록되면 다른 클래스에서 주입하여 사용할 수 있다.
* 명시적으로 Service Layer 라는것을 나타낸다.
* 비지니스 로직을 수행한다.
*/
@Service
public class MemoServiceImpl implements MemoService {
private final MemoRepository memoRepository;
public MemoServiceImpl(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
@Override
public MemoResponseDto saveMemo(MemoRequestDto requestDto) {
// 요청받은 데이터로 Memo 객체 생성 ID 없음
Memo memo = new Memo(requestDto.getTitle(), requestDto.getContents());
// Inmemory DB에 Memo 저장
Memo savedMemo = memoRepository.saveMemo(memo);
return new MemoResponseDto(savedMemo);
}
@Override
public List<MemoResponseDto> findAllMemos() {
// 전체 조회
List<MemoResponseDto> allMemos = memoRepository.findAllMemos();
return allMemos;
}
@Override
public MemoResponseDto findMemoById(Long id) {
// 식별자의 Memo가 없다면?
Memo memo = memoRepository.findMemoById(id);
// NPE 방지
if (memo == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
return new MemoResponseDto(memo);
}
@Override
public MemoResponseDto updateMemo(Long id, String title, String contents) {
// memo 조회
Memo memo = memoRepository.findMemoById(id);
// NPE 방지
if (memo == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
// 필수값 검증
if (title == null || contents == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The title and content are required values.");
}
// memo 수정
memo.update(title, contents);
return new MemoResponseDto(memo);
}
@Override
public MemoResponseDto updateTitle(Long id, String title, String contents) {
// memo 조회
Memo memo = memoRepository.findMemoById(id);
// NPE 방지
if (memo == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
// 필수값 검증
if (title == null || contents != null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The title and content are required values.");
}
memo.updateTitle(title);
return new MemoResponseDto(memo);
}
@Override
public void deleteMemo(Long id) {
// memo 조회
Memo memo = memoRepository.findMemoById(id);
// NPE 방지
if (memo == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
memoRepository.deleteMemo(id);
}
}
2. Database Version
더보기
package com.example.springbasicdatabase.service;
import com.example.springbasicdatabase.dto.MemoRequestDto;
import com.example.springbasicdatabase.dto.MemoResponseDto;
import com.example.springbasicdatabase.entity.Memo;
import com.example.springbasicdatabase.repository.MemoRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Optional;
/**
* Annotation @Service는 @Component와 같다, Spring Bean으로 등록한다는 뜻.
* Spring Bean으로 등록되면 다른 클래스에서 주입하여 사용할 수 있다.
* 명시적으로 Service Layer 라는것을 나타낸다.
* 비지니스 로직을 수행한다.
*/
@Service
public class MemoServiceImpl implements MemoService {
private final MemoRepository memoRepository;
public MemoServiceImpl(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
@Override
public MemoResponseDto saveMemo(MemoRequestDto requestDto) {
// 요청받은 데이터로 Memo 객체 생성
Memo memo = new Memo(requestDto.getTitle(), requestDto.getContents());
// 저장
return memoRepository.saveMemo(memo);
}
@Override
public List<MemoResponseDto> findAllMemos() {
// 전체 조회
List<MemoResponseDto> allMemos = memoRepository.findAllMemos();
return allMemos;
}
@Override
public MemoResponseDto findMemoById(Long id) {
// 식별자의 Memo가 없다면?
Optional<Memo> optionalMemo = memoRepository.findMemoById(id);
// NPE 방지
if (optionalMemo.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
return new MemoResponseDto(optionalMemo.get());
}
@Transactional
@Override
public MemoResponseDto updateMemo(Long id, String title, String contents) {
// 필수값 검증
if (title == null || contents == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The title and content are required values.");
}
// memo 수정
int updatedRow = memoRepository.updateMemo(id, title, contents);
// 수정된 row가 0개라면
if (updatedRow == 0) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No data has been modified.");
}
// 수정된 메모 조회
return new MemoResponseDto(memoRepository.findMemoById(id).get());
}
@Transactional
@Override
public MemoResponseDto updateTitle(Long id, String title, String contents) {
// 필수값 검증
if (title == null || contents != null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The title and content are required values.");
}
// memo 제목 수정
int updatedRow = memoRepository.updateTitle(id, title);
// 수정된 row가 0개 라면
if (updatedRow == 0) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No data has been modified.");
}
// 수정된 메모 조회
return new MemoResponseDto(memoRepository.findMemoById(id).get());
}
@Override
public void deleteMemo(Long id) {
// memo 삭제
int deletedRow = memoRepository.deleteMemo(id);
// 삭제된 row가 0개 라면
if (deletedRow == 0) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
}
}
5. Controller Layer
- 클라이언트의 요청을 받아 Service Layer에 전달
- API 엔드포인트 제공
1. Layered Architecture 버전
더보기
package com.example.springbasiclayered.controller;
import com.example.springbasiclayered.dto.MemoRequestDto;
import com.example.springbasiclayered.dto.MemoResponseDto;
import com.example.springbasiclayered.service.MemoService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.*;
/**
* Memo Controller
*/
@RestController // @Controller + @ResponseBody
@RequestMapping("/memos") // Prefix
public class MemoController {
// 주입된 의존성을 변경할 수 없어 객체의 상태를 안전하게 유지할 수 있다.
private final MemoService memoService;
/**
* 생성자 주입
* 클래스가 필요로 하는 의존성을 생성자를 통해 전달하는 방식
* @param memoService @Service로 등록된 MemoService 구현체인 Impl
*/
public MemoController(MemoService memoService) {
this.memoService = memoService;
}
/**
* 메모 생성 API
* @param : {@link MemoRequestDto} 메모 생성 요청 객체
* @return : {@link ResponseEntity<MemoResponseDto>} JSON 응답
*/
@PostMapping
public ResponseEntity<MemoResponseDto> createMemo(@RequestBody MemoRequestDto requestDto) {
return new ResponseEntity<>(memoService.saveMemo(requestDto), HttpStatus.CREATED);
}
/**
* 메모 전체 조회 API
* @return : {@link List<MemoResponseDto>} JSON 응답
*/
@GetMapping
public List<MemoResponseDto> findAllMemos() {
return memoService.findAllMemos();
}
/**
* 메모 단건 조회 API
* @param id 식별자
* @return : {@link ResponseEntity<MemoResponseDto>} JSON 응답
* @exception ResponseStatusException 식별자로 조회된 Memo가 없는 경우 404 Not Found
*/
@GetMapping("/{id}")
public ResponseEntity<MemoResponseDto> findMemoById(@PathVariable Long id) {
return new ResponseEntity<>(memoService.findMemoById(id), HttpStatus.OK);
}
/**
* 메모 전체 수정 API
* @param id 식별자
* @param : {@link MemoRequestDto} 메모 수정 요청 객체
* @return : {@link ResponseEntity<MemoResponseDto>} JSON 응답
* @exception ResponseStatusException 요청 필수값이 없는 경우 400 Bad Request, 식별자로 조회된 Memo가 없는 경우 404 Not Found
*/
@PutMapping("/{id}")
public ResponseEntity<MemoResponseDto> updateMemo(
@PathVariable Long id,
@RequestBody MemoRequestDto requestDto
) {
return new ResponseEntity<>(memoService.updateMemo(id, requestDto.getTitle(), requestDto.getContents()), HttpStatus.OK);
}
/**
* 메모 제목 수정 API
* @param id 식별자
* @param : {@link MemoRequestDto} 메모 수정 요청 객체
* @return : {@link ResponseEntity<MemoResponseDto>} JSON 응답
* @exception ResponseStatusException 요청 필수값이 없는 경우 400 Bad Request, 식별자로 조회된 Memo가 없는 경우 404 Not Found
*/
@PatchMapping("/{id}")
public ResponseEntity<MemoResponseDto> updateTitle(
@PathVariable Long id,
@RequestBody MemoRequestDto requestDto
) {
return new ResponseEntity<>(memoService.updateTitle(id, requestDto.getTitle(), requestDto.getContents()), HttpStatus.OK);
}
/**
* 메모 삭제 API
* @param id 식별자
* @return {@link ResponseEntity<Void>} 성공시 Data 없이 200OK 상태코드만 응답.
* @exception ResponseStatusException 식별자로 조회된 Memo가 없는 경우 404 Not Found
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMemo(@PathVariable Long id) {
memoService.deleteMemo(id);
// 성공한 경우
return new ResponseEntity<>(HttpStatus.OK);
}
}
2. Database Version
더보기
package com.example.springbasicdatabase.controller;
import com.example.springbasicdatabase.dto.MemoRequestDto;
import com.example.springbasicdatabase.dto.MemoResponseDto;
import com.example.springbasicdatabase.service.MemoService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
/**
* Memo Controller
*/
@RestController // @Controller + @ResponseBody
@RequestMapping("/memos") // Prefix
public class MemoController {
// 주입된 의존성을 변경할 수 없어 객체의 상태를 안전하게 유지할 수 있다.
private final MemoService memoService;
/**
* 생성자 주입
* 클래스가 필요로 하는 의존성을 생성자를 통해 전달하는 방식
* @param memoService @Service로 등록된 MemoService 구현체인 Impl
*/
public MemoController(MemoService memoService) {
this.memoService = memoService;
}
/**
* 메모 생성 API
* @param : {@link MemoRequestDto} 메모 생성 요청 객체
* @return : {@link ResponseEntity<MemoResponseDto>} JSON 응답
*/
@PostMapping
public ResponseEntity<MemoResponseDto> createMemo(@RequestBody MemoRequestDto requestDto) {
return new ResponseEntity<>(memoService.saveMemo(requestDto), HttpStatus.CREATED);
}
/**
* 메모 전체 조회 API
* @return : {@link List<MemoResponseDto>} JSON 응답
*/
@GetMapping
public List<MemoResponseDto> findAllMemos() {
return memoService.findAllMemos();
}
/**
* 메모 단건 조회 API
* @param id 식별자
* @return : {@link ResponseEntity<MemoResponseDto>} JSON 응답
* @exception ResponseStatusException 식별자로 조회된 Memo가 없는 경우 404 Not Found
*/
@GetMapping("/{id}")
public ResponseEntity<MemoResponseDto> findMemoById(@PathVariable Long id) {
return new ResponseEntity<>(memoService.findMemoById(id), HttpStatus.OK);
}
/**
* 메모 전체 수정 API
* @param id 식별자
* @param : {@link MemoRequestDto} 메모 수정 요청 객체
* @return : {@link ResponseEntity<MemoResponseDto>} JSON 응답
* @exception ResponseStatusException 요청 필수값이 없는 경우 400 Bad Request, 식별자로 조회된 Memo가 없는 경우 404 Not Found
*/
@PutMapping("/{id}")
public ResponseEntity<MemoResponseDto> updateMemo(
@PathVariable Long id,
@RequestBody MemoRequestDto requestDto
) {
return new ResponseEntity<>(memoService.updateMemo(id, requestDto.getTitle(), requestDto.getContents()), HttpStatus.OK);
}
/**
* 메모 제목 수정 API
* @param id 식별자
* @param : {@link MemoRequestDto} 메모 수정 요청 객체
* @return : {@link ResponseEntity<MemoResponseDto>} JSON 응답
* @exception ResponseStatusException 요청 필수값이 없는 경우 400 Bad Request, 식별자로 조회된 Memo가 없는 경우 404 Not Found
*/
@PatchMapping("/{id}")
public ResponseEntity<MemoResponseDto> updateTitle(
@PathVariable Long id,
@RequestBody MemoRequestDto requestDto
) {
return new ResponseEntity<>(memoService.updateTitle(id, requestDto.getTitle(), requestDto.getContents()), HttpStatus.OK);
}
/**
* 메모 삭제 API
* @param id 식별자
* @return {@link ResponseEntity<Void>} 성공시 Data 없이 200OK 상태코드만 응답.
* @exception ResponseStatusException 식별자로 조회된 Memo가 없는 경우 404 Not Found
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMemo(@PathVariable Long id) {
memoService.deleteMemo(id);
// 성공한 경우
return new ResponseEntity<>(HttpStatus.OK);
}
}
6. 전체적인 흐름
- 클라이언트 요청 → Controller
- 비즈니스 로직 수행 → Service
- 데이터 저장/조회 → Repository
- 응답 반환 → Controller → 클라이언트
7. 정리
- 계층역할
Entity 데이터 모델 DTO 데이터 전송 객체 Repository DB와 직접 소통하는 계층 Service 비즈니스 로직 담당 계층 Controller HTTP 요청을 처리하고 응답을 반환하는 계층