3. 해당 패키지에서 @Component 또는 Annotation이 붙은 클래스를 찾고, Spring 컨테이너에 빈으로 등록
4. 의존성 주입을 통해서 등록된 빈은 다른 빈과 연결됨
@Component 종류
@Component
- 스프링에게 해당 객체를 Bean으로 등록하겠다는 의미
@Service
- 스프링에게 해당 객체를 Bean으로 등록하면서 서비스 계층이라고 알리는 의미
@Repository
- 스프링에게 해당 객체를 Bean으로 등록하면서 리포지토리 계층이라고 알리는 의미
@Controller
- 스프링에게 해당 객체를 Bean으로 등록하면서 컨트롤러 계틍이라고 알리는 의미
실제로 @Service를 까보면 안에 @Component가 있음
Bean 충돌
자동 Bean 등록 = (@ComponentScan, @Component)
@Component 이 있는 클래스의 앞글자만 소문자로 변경하여 Bean 이름으로 등록
@ComponentScan 을 통해 @Component로 설정된 클래스를 찾는 것
수동 Bean 등록(@Configuration, @Bean)
@Configuration 이 있는 클래스를 Bean으로 등록하고 해당 클래스를 파싱해서 @Bean 이 있는 메서드를 찾아 Bean을 생성
// 인터페이스
public interface TestService {
void doSomething();
}
// 인터페이스 구현체
public class TestServiceImpl implements TestService {
@Override
public void doSomething() {
System.out.println("Test Service 메서드 호출");
}
}
// 수동으로 빈 등록
@Configuration
public class AppConfig {
// TestService 타입의 Spring Bean 등록
@Bean
public TestService testService() {
// TestServiceImpl을 Bean으로 등록
return new TestServiceImpl();
}
}
// Spring Bean으로 등록이 되었는지 확인
public class MainApp {
public static void main(String[] args) {
// Spring ApplicationContext 생성 및 설정 클래스(AppConfig) 등록
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 등록된 TestService 빈 가져오기
TestService service = context.getBean(TestService.class);
// 빈 메서드 호출
service.doSomething();
}
}
자동 Bean 등록 VS 자동 Bean 등록 충돌 : ConflictingBeanDefinitionException 발생
자동 Bean 등록VS 수동 Bean 등록 충돌 : 수동이 자동을 오버라이딩 함
Singleton Pattern
클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 디자인 패턴
스프링 빈을 싱글톤 패턴으로 적용하지 않으면, 다수의 고객이 요청할 때마다 생성하고 소멸되기 때문에 메모리 낭비
싱글톤 패턴을 직접 구현하려면 복잡하고 솔리드 원칙을 위반하게 됨 -> Spring Container는 싱글톤 패턴의 문제점들을 해결하면서 객체를 싱글톤으로 관리
Spring Bean은 인스턴스가 1개 이기 때문에 항상 무상태(stateless)로 설계를 해야함
IOC(제어의 역전, Inversion Of Control)
객체의 생성과 관리 권한을 개발자가 아닌 Spring 컨테이너가 담당하는 것
(제어를 내가 아니라 프로그램이 했으니깐, 역전된 것!)
DI(의존성 주입, Dependency Injection)
한 객체가 다른 객체를 사용할 때, 해당 객체를 직접 생성하지 않고 Spring이 주입해주는 방식
User 클래스는 사용자 정보, 로그인기능, 사용자정보를 데이터베이스에 저장하는 내용 3가지의 기능을 모두 수행하고 있음
public class User {
private String name; // 사용자 정보
public void login() { /* 로그인 기능 */ }
public void saveUser() { /* 데이터베이스 저장 기능 */ }
}
단일 책임 원칙 적용
User는 사용자의 정보만 관리하고, 서비스는 로그인 기능을 담당하고, 리포지토리는 데이터베이스에 사용자 정보를 저장하는 기능 담당하는 책임을 가짐
public class User { /* 사용자 정보 관리 */ }
public class AuthService {
public void login(User user) { /* 로그인 기능 */ }
}
public class UserRepository {
public void saveUser(User user) { /* 데이터베이스 저장 */ }
}
개방 폐쇄 원칙 OCP(Open Closed Principle)
확장에는 열려 있고, 수정에는 닫혀 있어야 함 = 새로운 기능을 추가할 때 기존 코드의 수정 없이 추가할 수 있어야 함
예시
개방 폐쇄 원칙 위반
새로운 도형이 추가 될 때마다, AreaCalculator 클래스의 calculate 메서드 내부를 if문을 사용하여 수정해야 함
public class Shape {
public String type;
}
public class AreaCalculator {
public double calculate(Shape shape) {
if (shape.type.equals("circle")) {
return /* 원의 넓이 계산 */;
} else if (shape.type.equals("square")) {
return /* 사각형의 넓이 계산 */;
}
}
}
개방 폐쇄 원칙 적용
새로운 도형이 추가되어도, Shape인터페이스를 상속하여 이를 구현하면 되기 때문에 AreaCalculator를 수정할 필요가 없음
다형성을 사용 = 역할(도형)과 구현(원,삼각형)을 분리한 것
public interface Shape {
double calculateArea();
}
public class Circle implements Shape {
public double calculateArea() { return /* 원의 넓이 계산 */; }
}
public class Square implements Shape {
public double calculateArea() { return /* 사각형의 넓이 계산 */; }
}
public class AreaCalculator {
public double calculate(Shape shape) {
return shape.calculateArea();
}
}
리스코프 치환 원칙 LSP(Liskov Substitution Principle)
자식 클래스는 부모 클래스를 대체 할 수 있어야 함
부모 클래스를 사용하는 곳에서 자식 클래스를 사용해도 프로그램 동작에 문제가 없어야 함
예시
리스코프 치환 원칙 위반
ElectricCar는 Car 클래스를 상속 받았지만, accelerate() 를 사용할 수 없음
class Car {
public void accelerate() {
System.out.println("자동차가 휘발유로 가속합니다.");
}
}
class ElectricCar extends Car {
@Override
public void accelerate() {
throw new UnsupportedOperationException("전기차는 이 방식으로 가속하지 않습니다.");
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.accelerate(); // "자동차가 가속합니다."
Car electricCar = new ElectricCar();
electricCar.accelerate(); // UnsupportedOperationException 발생
}
}
리스코프 치환 원칙 적용
가속 기능(역할)을 인터페이스로 분리
interface Acceleratable {
void accelerate();
}
class Car implements Acceleratable {
@Override
public void accelerate() {
System.out.println("내연기관 자동차가 가속합니다.");
}
}
class ElectricCar implements Acceleratable {
@Override
public void accelerate() {
System.out.println("전기차가 배터리로 가속합니다.");
}
}
public class Main {
public static void main(String[] args) {
Acceleratable car = new Car();
car.accelerate(); // "내연기관 자동차가 가속합니다."
Acceleratable electricCar = new ElectricCar();
electricCar.accelerate(); // "전기차가 배터리로 가속합니다."
}
}
인터페이스 분리 원칙 ISP(Interface Segregation Principle)
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 함
예시
인터페이스 분리 원칙 위반
Dog은 사용하지 않는 fly 메서드를 구현해야 함
public interface Animal {
void fly();
void run();
void swim();
}
public class Dog implements Animal {
public void fly() { /* 사용하지 않음 */ }
public void run() { /* 달리기 */ }
public void swim() { /* 수영 */ }
}
인터페이스 분리 원칙 적용
public interface Runnable {
void run();
}
public interface Swimmable {
void swim();
}
public class Dog implements Runnable, Swimmable {
public void run() { /* 달리기 */ }
public void swim() { /* 수영 */ }
}
의존관계 역전 원칙 DIP(Dependency Inversion Principle)
구체적인 클래스에 의존하지 말고 인터페이스나 추상 클래스에 의존해야 함
예시
의존관계 역전 원칙 위반
SMS 알림과 같은 기능이 추가되면 NotificationService 는 수정되어야 함
// Email 알림 클래스
class EmailNotifier {
public void sendEmail(String message) {
System.out.println("Email 알림: " + message);
}
}
// 알림 시스템
class NotificationService {
private EmailNotifier emailNotifier;
public NotificationService() {
// 구체적인 클래스인 EmailNotifier에 의존
this.emailNotifier = new EmailNotifier();
}
public void sendNotification(String message) {
emailNotifier.sendEmail(message);
}
}
public class Main {
public static void main(String[] args) {
NotificationService service = new NotificationService();
service.sendNotification("안녕하세요! 이메일 알림입니다.");
}
}
적용
// 알림 인터페이스(추상화)
interface Notifier {
void send(String message);
}
// Email 알림 클래스
class EmailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("Email 알림: " + message);
}
}
// SMS 알림 클래스
class SMSNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("SMS 알림: " + message);
}
}
// 알림 서비스 (높은 수준 모듈)
class NotificationService {
// 추상화된 인터페이스에 의존
private Notifier notifier;
// 의존성 주입 (생성자를 통해 주입)
public NotificationService(Notifier notifier) {
this.notifier = notifier;
}
public void sendNotification(String message) {
// notifier가 어떤 구현체인지 상관하지 않음
notifier.send(message);
}
}
public class Main {
public static void main(String[] args) {
// Email 알림을 사용
Notifier emailNotifier = new EmailNotifier();
NotificationService emailService = new NotificationService(emailNotifier);
emailService.sendNotification("안녕하세요! 이메일 알림입니다.");
// SMS 알림을 사용
Notifier smsNotifier = new SMSNotifier();
NotificationService smsService = new NotificationService(smsNotifier);
smsService.sendNotification("안녕하세요! SMS 알림입니다.");
}
}
다형성의 한계 : OCP, DIP의 위반과 Spring
OCP 위반 (수정없이 기능을 확장)
구현 객체를 변경하기 위해서는 해당 코드를 사용하는 클라이언트측의 코드를 변경해야 함
// Circle을 계산하는 경우
public class Main {
public static void main(String[]) {
AreaCalculator areaCalculator = new AreaCalculator();
Circle circle = new Circle();
areaCalculator.calculate(circle);
}
}
// Square를 계산하는 경우
public class Main {
public static void main(String[]) {
AreaCalculator areaCalculator = new AreaCalculator();
// Circle circle = new Circle();
Square square = new Square();
areaCalculator.calculate(square);
}
}
DIP 위반 (구체화 말고 추상화에 의존)
클라리언트가 OrderService(=인터페이스)를 의존하고 있지만, RateDiscountPolicy라는 구현클래스에도 의존하고 있으므로 위반
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
⭐️ 결론 : Spring => OCP, DIP를 IOC, DI를 통해 가능하도록 만들어준다. ⭐️
@Getter
@AllArgsConstructor
public class Member {
private Long id;
private String name;
private String email;
private LocalDateTime createDate;
private LocalDateTime updatedDate;
}
일정 엔티티
@Getter
@AllArgsConstructor
public class Schedule {
private Long id;
private String content;
private Long memberId;
private String password;
private LocalDateTime createDate;
private LocalDateTime modifiedDate;
}
연관관계 적용 - 저장
일정 저장시에 memberId값도 함께 추가하여 등록 (scheule table에 들어갈 member table의 pk값)
@Getter
public class ScheduleSaveRequestDto {
private Long memberId;
// NotBlank를 사용해야 null, 빈 문자열(""), 공백(" ") 모두 허용하지 않음
@NotBlank
@Max(value = 200)
private String content;
@NotBlank
private String password;
}
리포지토리에서 해당 값을 함께 담아서 schedule table에 insert!
@Override
public ScheduleResponseDto saveSchedule(Schedule schedule) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("schedule").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("content", schedule.getContent());
parameters.put("member_id", schedule.getMemberId());
...
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
return new ScheduleResponseDto(key.longValue(), schedule.getContent(), schedule.getMemberId(), LocalDateTime.now(), LocalDateTime.now());
}
연관관계 적용 - 조회
일정 조회시에 작성자의 이름을 함께 가져오기
작성자의 이름은 Member 테이블에 있기 때문에 join하여 조회!
@Override
public List<SchedulePagingResponseDto> findSchedules(Long offset, int size) {
return jdbcTemplate.query("select s.id, s.content, m.name, s.created_at, s.updated_at from schedule as s inner join member as m on s.member_id = m.id limit ?, ?", scheduleRowMapperV4(), offset, size);
}
작성자의 이름이 응답객체에 포함되기 때문에 이름을 포함한 전용 Mapper 생성
public RowMapper<SchedulePagingResponseDto> scheduleRowMapperV4(){
return new RowMapper<SchedulePagingResponseDto>() {
@Override
public SchedulePagingResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
return new SchedulePagingResponseDto(
rs.getLong("id"),
rs.getString("content"),
rs.getString("name"),
rs.getTimestamp("created_at").toLocalDateTime(),
rs.getTimestamp("updated_at").toLocalDateTime()
);
}
};
}
페이징 적용
고민했던 점
LIMIT의 개수는 고정적이기에 상관없지만 OFFSET의 경우는 어떤 식으로 지정해야 할지 고민됐음
OFFSET의 공식을 사용해서 해결해야 하나 고민이 많이 됐는데 Pageable객체에서 사용할 수 있는게 뭐가 있는지 찾아보다 getOffet을 발견하고 해결!
페이징 쿼리
schdule테이블의 행을 10번째부터 시작해서 20개를 가져오겠다는 의미
LIMIT : 가져올 행의 개수
OFFSET : 건너뛸 행의 개수
SELECT *
FROM schedule
LIMIT 10, 20;
구현
페이지의 값과 각 페이지별 크기를 쿼리 파라미터로 입력받음
@GetMapping("/paged")
public ResponseEntity<Page<SchedulePagingResponseDto>> list(@RequestParam("page") int page, @RequestParam("size") int size){
Page<SchedulePagingResponseDto> pagedList = scheduleService.findSchedulePageList(page, size);
return new ResponseEntity<>(pagedList, HttpStatus.OK);
}
Pageable : 페이징 정보를 담는 인터페이스
PageRequest : Pageable구현체
PageImpl<T> : List<T>를 Page<T>로 변환
PageRequest를 이용하여 page와 size값을 세팅한 후에 Pageable로 생성
@Override
public Page<SchedulePagingResponseDto> findSchedulePageList(int page, int size) {
int count = scheduleRepository.findScheduleSize();
Pageable pageable = PageRequest.of(page, size);
return new PageImpl<>(scheduleRepository.findSchedules(pageable.getOffset(), pageable.getPageSize()), pageable, count);
}
PageImpl객체를 생성할 때 데이터의 전체 값을 인자로 함께 넣어줘야 정확한 페이징이 이루어짐
new PageImpl<>(scheduleRepository.findSchedules(pageable.getOffset(), pageable.getPageSize()), pageable, count);
리포지토리에서 Pageable객체를 이용하여 offset값과 size값으로 페이징 쿼리 실행 후 반환
@Override
public List<SchedulePagingResponseDto> findSchedules(Long offset, int size) {
return jdbcTemplate.query("select s.id, s.content, m.name, s.created_at, s.updated_at from schedule as s inner join member as m on s.member_id = m.id limit ?, ?", scheduleRowMapperV4(), offset, size);
}
사용자가 잘못된 경우로 입력했을 때 MethodArgumentTypeMismatchException 발생
해당 예외가 발생했을 때 직접 만든 에러 객체를 던지고 싶었기에 파라미터를 string으로 받고 검증 로직을 따로 추가해서 잘못된 경우로 입력했을 때InvalidRequestException(직접만든 예외)가 발생하도록 함
쿼리 파라미터 검증
ValidationUtils class를 만들어서 쿼리 파라미터의 값을 모두 stirng형태로 받고, 검증을 수행
검증이 완료되면(사용자가 숫자만 입력한 경우) 해당 값을 Long으로 바꾸고 반환
// 사용자의 입력값이 숫자로만 이루어 지지 않았을 경우에, 검증에 사용하는 Util Class
public class ValidationUtils {
static final String NUMBER_REG = "[0-9]+";
public static Long validNumberInputValue(String number) throws InvalidRequestException {
if (!number.matches(NUMBER_REG)){
throw new InvalidRequestException(INVALID_REQUEST_EXCEPTION);
}
return Long.parseLong(number);
}
}
Dto 검증
@Valid를 사용하여 검증
@Email을 사용하는 경우에는 공백이 입력 가능하기 때문에 정규표현식으로 이메일 형식 적용
NotBlank를 사용해야 null, 빈 문자열(""), 공백(" ") 모두 허용하지 않음
@Getter
public class ScheduleSaveRequestDto {
private Long memberId;
// NotBlank를 사용해야 null, 빈 문자열(""), 공백(" ") 모두 허용하지 않음
@NotBlank
@Max(value = 200)
private String content;
@NotBlank
private String password;
}
@Getter
public class MemberSaveRequestDto {
private String name;
// @Email을 사용하는 경우에는 공백이 입력 가능하기 때문에 정규표현식으로 이메일 형식 적용
@Pattern(regexp = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,6}$", message = "이메일 형식이 올바르지 않습니다.")
private String email;
}
예외 발생 처리
에러 객체와 관련된 패키지
에러 객체가 발생한 경우 사용하는 응답 객체를 공통으로 관리하기 위해 생성
@Getter
@Builder
public class ErrorResponse {
private HttpStatus status;
private String message;
public ErrorResponse(HttpStatus status, String message){
this.status = status;
this.message = message;
}
public static ResponseEntity<ErrorResponse> toResponseEntity(ErrorCode e) {
return ResponseEntity.status(e.getStatus().value())
.body(ErrorResponse.builder()
.status(e.getStatus())
.message(e.getMessage())
.build());
}
}
에러 객체의 코드를 관리하기 위한 enum class
@Getter
@AllArgsConstructor
public enum ErrorCode {
INVALID_REQUEST_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 요청 정보입니다."),
INVALID_PASSWORD_EXCEPTION(HttpStatus.UNAUTHORIZED, "올바르지 않은 비밀번호입니다."),
SCHEDULE_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "존재하지 않는 일정입니다."),
MEMBER_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다.");
private final HttpStatus status;
private final String message;
}
컨트롤러에서 발생하는 에러를 관리하는 ExceptionController
@RestControllerAdvice(assignableTypes = ScheduleController.class)
@Slf4j
public class ScheduleExceptionController {
/*
* 일정이 없는 경우 발생하는 에러객체를 처리
*/
@ExceptionHandler(ScheduleNotFoundException.class)
public ResponseEntity<ErrorResponse> handleScheduleNotFoundException(ScheduleNotFoundException e) {
log.error("[ScheduleExceptionHandler] ScheduleNotFoundException = {}, class = {}", e.getErrorCode().getMessage(), e.getClass());
// return ResponseEntity.status(e.getErrorCode().getStatus()).body(new ErrorResponse(e.getErrorCode().getStatus(), e.getErrorCode().getMessage()));
return ErrorResponse.toResponseEntity(e.getErrorCode());
}
@Override
public Schedule findScheduleByIdOrElseThrow(Long id) {
List<Schedule> result = jdbcTemplate.query("select * from schedule where id = ?", scheduleRowMapperV2(), id);
return result.stream().findAny().orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id));
}
public RowMapper<Schedule> scheduleRowMapperV2(){
return new RowMapper<Schedule>() {
@Override
public Schedule mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Schedule(
rs.getLong("id"),
rs.getString("content"),
rs.getString("author"),
rs.getTimestamp("created_at").toLocalDateTime(),
rs.getTimestamp("updated_at").toLocalDateTime()
);
}
};
}
일정 수정하기
Path로 수정할 일정의 key값을 받고, ScheduleRequestDto를 통해서 수정할 내역을 요청받음
@PatchMapping("/{id}")
public ResponseEntity<ScheduleResponseDto> updateSchedule(@PathVariable Long id, @RequestBody ScheduleRequestDto requestDto) {
return new ResponseEntity<>(scheduleService.updateSchedule(id, requestDto.getPassword(), requestDto.getContent(), requestDto.getAuthor()), HttpStatus.OK);
}
key값을 통해서 수정할 엔티티의 비밀번호와 ScheduleRequestDto에서 입력받은 비밀번호와 일치하는지 확인함.
@Override
public Schedule findSchedulePasswordByIdOrElseThrow(Long id) {
List<Schedule> result = jdbcTemplate.query("select * from schedule where id = ?", scheduleRowMapperV3(), id);
return result.stream().findAny().orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id));
}
public RowMapper<Schedule> scheduleRowMapperV3(){
return new RowMapper<Schedule>() {
@Override
public Schedule mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Schedule(
rs.getLong("id"),
rs.getString("content"),
rs.getString("author"),
rs.getString("password"),
rs.getTimestamp("created_at").toLocalDateTime(),
rs.getTimestamp("updated_at").toLocalDateTime()
);
}
};
}
만약 일치하지 않는다면 HttpStatus.BAD_REQUEST 로 응답
@Override
public ScheduleResponseDto updateSchedule(Long id, String password, String content, String author) {
// 수정할 일정의 비밀번호 조회
Schedule schedule = scheduleRepository.findSchedulePasswordByIdOrElseThrow(id);
if (!schedule.getPassword().equals(password)){
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다");
}
int updatedRow = scheduleRepository.updateSchedule(id, content, author);
if (updatedRow == 0) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "수정된 데이터가 없습니다");
}
Schedule updatedSchedule = scheduleRepository.findScheduleByIdOrElseThrow(id);
return new ScheduleResponseDto(updatedSchedule);
}
일정 삭제하기
Path로 수정할 일정의 key값을 받아서 삭제
수정 또한 update사용!
@Override
public int deleteSchedule(Long id) {
return jdbcTemplate.update("delete from schedule where id = ?", id);
}
Data Transfer Object, 즉, HTTP 메시지를 전달하고 스프링에서 계층간의 데이터를 전달할 때 사용하는 객체
DTO의 사용 이유
1. Entity와 DTO의 책임을 명확하게 분리하기 위함
Entity는 ORM을 해주기 위한 객체
DTO는 계층간의 데이터를 전송해 주기 위한 객체 & Client에게 Entity정보를 숨기기 위함
-> DTO 대신 Entity를 사용하게 된다면 Entity가 가지고 있는 민감한 정보(주민번호, 비밀번호,,)가 노출 될 수 있음
2. DB 구조가 변경됐을 때 코드의 유지보수에 용이하기 위함
예시
MemberController
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/member")
public class MemberController {
private final MemberService memberService;
@GetMapping("/{memberId}")
public MemberResponseDto getMemberInfo(
@PathVariable Long memberId
) {
return memberService.getMemberInfo(memberId);
}
}
MemberService
MemberService
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
public MemberResponseDto getMemberInfo(Long memberId) {
Member member = memberRepository.findById(memberId).orElseThrow(NullPointerException::new);
// Entity To DTO
MemberResponseDto responseDto = new MemberResponseDto(member);
return responseDto;
}
}
MemberResponseDto
@Getter
public class MemberResponseDto {
private String username;
private int age;
public MemberResponseDto(Member member) {
this.username = member.getUsername();
this.age = member.getAge();
}
}
Member & MemberRepository
@Getter
@Entity
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private int age;
private String rrn; // 주민번호, Resident Registration Number
}
public interface MemberRepository extends JpaRepository<Member, Long> {}