2025.02.06(목)
필수
Lv 0. API 명세 및 ERD 작성
이 부분은 과제 구현 후 작성하고자 한다.
Lv 1. 일정 CRUD
application.properties 설정 주의하기
2025-02-06T16:28:25.400+09:00 INFO 5078 --- [Schedule-Develop] [ main] org.hibernate.orm.connections.pooling : HHH10001005: Database info: Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)'] Database driver: undefined/unknown Database version: 9.2 Autocommit mode: undefined/unknown Isolation level: undefined/unknown Minimum pool size: undefined/unknown Maximum pool size: undefined/unknown
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.DataIntegrityViolationException: not-null property references a null or transient value: com.example.scheduledevelop.domain.entity.Schedule.username] with root cause
위와 같은 예외가 발생했다.
package 안에 있는 코드는 강의 실습 코드를 바탕으로 작성했으므로 큰 실수는 없을 것이라고 생각했다.
따라서 아마도 JPA 설정은 처음이라 설정 부분에서 빠진 부분이 있을 것이라고 판단했다.
application.properties에 아래와 같은 코드를 추가했더니 잘 작동했다.
spring.application.name=Schedule-Develop
spring.datasource.url=jdbc:mysql://localhost:3306/schedule-develop
spring.datasource.username=계정
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
일정 생성 후, DB에서 생성일, 수정일 null 발생
위와 같이 생성일과 수정일에 null이 되었다.
@EnableJpaAuditing
@SpringBootApplication
public class ScheduleDevelopApplication {
public static void main(String[] args) {
SpringApplication.run(ScheduleDevelopApplication.class, args);
}
}
원인은 ScheduleDevelopApplication 파일에
@EnableJpaAuditing을 하지 않은 것이 문제였다.
위와 같이 설정하고 다시 실행하면 아래와 같이 DB에 잘 들어온 것을 확인할 수 있다.
위와 같은 일을 겪고 나니 JPA 설정에 대해서는 적어도 잊어버리지 않은 것 같다는 생각이 들었다.
일정 단건 조회 구현 시, 컴파일 에러 발생
@GetMapping("/{scheduleId}")
public RequestEntity<ScheduleResponseDto> findById(@PathVariable("scheduleId") Long id){
ScheduleResponseDto scheduleResponseDto = scheduleService.findById(id);
return new RequestEntity<>(scheduleResponseDto,HttpStatus.OK);
}
위와 같이 responseEntity가 아닌 requestEntity를 사용해서 발생한 오류였다.
Response를 반환해야 한다고 머리로는 생각했으면서도
손은 그렇게 작성하지 못했고
눈은 그것을 판별하지 못했지만
다행히? 컴파일 에러로 보여서 잘 수정할 수 있었다.
Lv 2. 유저 CRUD
오늘 유저 자체의 CRUD는 구현을 완료했으나,
일정은 이제 작성 유저명 필드 대신 유저 고유 식별자 필드를 가집니다.
위의 부분에 대해서
일정 테이블과 유저 테이블의 연관 관계 매핑에 대해서
좀 더 고민해보고 구현할 예정이다.
2025.02.07(금)
필수
Lv 2. 유저 CRUD
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.NullPointerException: Cannot invoke "com.example.scheduledevelop.domain.entity.Member.getName()" because the return value of "com.example.scheduledevelop.domain.entity.Schedule.getMember()" is null] with root cause
생성자 주입이나 Setter 주입을 통해 Member 값을 넣어줘야 하는데 그렇게 하지 않아서 발생했다.
public Schedule(String title, String contents, Member member) {
this.title = title;
this.contents = contents;
this.member = member;
}
따라서 위와 같이 Schedule 클래스의 생성자에 member도 포함시키고
Member findMember = memberRepository.findMemberByEmailOrElseThrow(memberEmail);
Schedule schedule = new Schedule(title, contents, member);
Service에서 email로 해당 Member를 찾아온 후,
생성자에 member 값을 세팅해주었다.
필수
Lv 4. 로그인(인증)
일정 생성, 수정, 삭제시 로그인 Session 값을 가져오는 방법
로그인 기능 자체는 구현을 완료했다.
하지만 로그인 후, 일정을 생성, 수정, 삭제할 때 해당 세션 값을 가져오는 방법에 대해 고민이 생겼다.
그래서 숙련 Spring 2주차 강의에서 Session 부분을 다시 들으며 복습했다.
그리고 강의에서 해답을 찾을 수 있었다.
Spring에서는 Session을 쉽게 다루도록 @SessionAttribute라는 어노테이션이 제공된다고 한다.
사실 다른 방법도 있지만 더 깔끔해 보여서 나는 이 어노테이션을 사용하기로 햇다.
@SessionAttribute
- request.getSession(true); 와는 다르게 Session을 새로 생성하는 기능은 없다.
- 이미 로그인이 완료된 사용자를 찾는 경우 즉, Session이 있는 경우에 사용한다.
이미 AuthController에서 Session을 생성하기 때문에 생성 기능은 필요하지 않았다.
일정 Controller에서는 로그인 후, 즉 Session이 있는 경우에 사용하면 되는 것이었다.
@PostMapping
public ResponseEntity<ScheduleResponseDto> save(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
@RequestBody ScheduleSaveRequestDto requestDto) {
// 생성 로직
ScheduleResponseDto scheduleResponseDto
= scheduleService.save(requestDto.getTitle(), requestDto.getContents(),loginMember);
return new ResponseEntity<>(scheduleResponseDto, HttpStatus.CREATED);
}
따라서 Controller에서 해당 Session을 매개변수로 받아서 Member타입의 변수에 저장했다.
그리고 Service에 같이 매개변수로 넣었다.
@Transactional
public ScheduleResponseDto save(String title, String contents, Member member) {
Schedule schedule = new Schedule(title, contents, member);
Schedule savedSchedule = scheduleRepository.save(schedule);
return new ScheduleResponseDto(
savedSchedule.getId(),
savedSchedule.getTitle(),
savedSchedule.getContents(),
savedSchedule.getMember().getName(),
savedSchedule.getMember().getEmail()
);
}
이를 Service에서 Schedule 생성자를 통해 loginMember 값을 주입할 수 있었다.
작성자만 일정을 수정, 삭제하게 하는 방법
위에서 매개변수로 받아온 loginMember 변수를 활용하기로 했다.
각각의 id가 동일한지 비교하는 로직을 만들고
같지 않다면 예외처리를 하고,
같다면 수정, 삭제 로직을 수행하도록 할 계획이었다.
그러나 위와 같은 생각으로 구현을 하고 Postman을 통해 테스트를 하는데
이상한 점을 발견하게 되었다.
분명 작성자인데 수정, 삭제가 작동하지 않는 것이었다.
System.out.println("findMemberId = " + findMemberId);
위와 같이 print를 통해 각각의 id를 조회하고 나니 원인을 찾을 수 있었다...
memberId와 scheduleId를 비교하고 있으니 둘이 다를 수 밖에...
따라서 아래와 같이 findMember라는 새로운 Service 메서드를 만들어서
scheduleId에 해당하는 Member를 찾고 그 Member의 아이디를 찾아서 이를 비교했다.
@PatchMapping("/{scheduleId}")
public ResponseEntity<Void> updateTitleAndContents(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
@PathVariable("scheduleId") Long id,
@RequestBody UpdateTitleAndContentsRequestDto requestDto
) {
Member findMember = scheduleService.findMember(id);
Long findMemberId = findMember.getId();
// 작성자만 수정 가능
if (!loginMember.getId().equals(findMemberId)){
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
// 수정 로직
scheduleService.updateTitleContents(id, requestDto.getTitle(), requestDto.getContents());
return new ResponseEntity<>(HttpStatus.OK);
}
@DeleteMapping("/{scheduleId}")
public ResponseEntity<Void> delete(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
@PathVariable("scheduleId") Long id
){
Member findMember = scheduleService.findMember(id);
Long findMemberId = findMember.getId();
// 작성자만 삭제 가능
if (!loginMember.getId().equals(findMemberId)){
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
// 삭제 로직
scheduleService.delete(id);
return new ResponseEntity<>(HttpStatus.OK);
}
오늘은 계획대로 필수 과제 구현을 모두 끝내게 되어서 뿌듯하다.
다음 주에는 도전과제를 도전할 계획이다.
다음 주도 화이팅이다!
2025.02.10(월)
도전
Lv 6. 비밀번호 암호화
로그인 시 입력한 비밀번호와 암호화된 비밀번호 비교 방법
회원 가입 시 비밀번호를 암호화하는 방법은 아래와 같이 진행했다.
@PostMapping("/signup")
public ResponseEntity<SingUpResponseDto> save(@Valid @RequestBody SignUpRequestDto requestDto){
String encyptPassword = passwordEncoder.encode(requestDto.getPassword()); //비밀번호 암호화
SingUpResponseDto singUpResponseDto
= memberService.save(requestDto.getName(),requestDto.getEmail(),encyptPassword);
return new ResponseEntity<>(singUpResponseDto, HttpStatus.CREATED);
}
그러나 이후 로그인 시 입력한 비밀번호와 암호화된 비밀번호를 어떻게 비교하면 좋을지에 대해 고민이 들기 시작했다.
처음에는 같은 비밀번호를 암호화하면 같은 암호화된 값이 나오는 줄 알고 아래와 같이 진행했었다.
그러나 생각대로 되지 않아, 로그로 확인해보니 DB에 저장된 암호화된 비밀번호와 다른 것을 발견할 수 있었다.
String encyptPassword = passwordEncoder.encode(requestDto.getPassword()); //비밀번호 암호화
log.info(encyptPassword);
그렇다면 전처럼 equals와 같은 방법으로 비교할 수는 없을 것이다.
다른 방법은 뭐가 있을까? 바로 matches 메서드를 사용했다.
email(아이디)로 멤버 조회 -> 만약 멤버가 null이면 401 에러 처리
-> 멤버 DB password와 입력한 비밀번호가 같은지 비교(matches)
-> 만약 같으면 email과 암호화된 DB password로 로그인 멤버 조회
-> 만약 같지 않으면 401 에러 처리
@PostMapping("/login")
public ResponseEntity<String> login(@Valid @RequestBody LoginRequestDto requestDto, HttpServletRequest request){
Member member= authService.findByEmail(requestDto.getEmail());
if (member == null){
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
String encyptPassword = member.getPassword();
if (passwordEncoder.matches(requestDto.getPassword(),encyptPassword)){
Member loginMember = authService.login(requestDto.getEmail(), encyptPassword);
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return new ResponseEntity<>(HttpStatus.OK);
}else {
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
}
Lv 7. 댓글 CRUD
일정 id에 해당하는 댓글 조회 기능 구현
댓글과 일정은 연관관계를 가진다.
sql에 익숙한 나로서는 join과 같은 sql문을 작성하지 않고도
댓글 테이블에서 일정 테이블 id로 조회하는 것을 찾기가 쉽지 않았다.
List<Comment> commentList = commentRepository.findAllBySchedule_Id(schedule.getId());
하지만 JPA 메서드를 찾아보니 위와 같은 기능을 발견할 수 있었다.
간단한 쿼리를 메서드를 찾아보면 된다고 튜터님께서 말씀하셨는데
정말 그런 것 같다.
사실 레벨 8까지 진도는 모두 나갔지만
아직 튜터님의 확인을 받지 못했기 때문에
내가 놓친 조건이 있을 수도 있고, 잘못 구상한 부분도 있을 수도 있다.
그래서 내일 오전 중에 튜터님께 문의 드릴 예정이다.
오늘 하루도 스스로에게 고생 많았다는 말을 해주고 싶다.
2025.02.11(화)
일정관리 Develop 리팩토링
리팩토링 내용
- 패키지 엔티티별로 분리
- 일정, 댓글 responseDto에 수정일, 생성일 필드 추가
- 메서드명 수정
- Controller에 있는 비즈니스 로직 -> Service로 옮기기
/* 수정 전 */
@PatchMapping("/{scheduleId}")
public ResponseEntity<Void> updateTitleAndContents(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
@PathVariable("scheduleId") Long id,
@Valid @RequestBody UpdateTitleAndContentsRequestDto requestDto
) {
Member findMember = scheduleService.findMember(id);
Long findMemberId = findMember.getId();
// 작성자만 수정 가능
if (!loginMember.getId().equals(findMemberId)){
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
// 수정 로직
scheduleService.updateTitleContents(id, requestDto.getTitle(), requestDto.getContents());
return new ResponseEntity<>(HttpStatus.OK);
}
/* 수정 후 */
@PatchMapping("/schedules/{scheduleId}")
public ResponseEntity<ScheduleResponseDto> updateTitleAndContents(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
@PathVariable("scheduleId") Long id,
@Valid @RequestBody UpdateTitleAndContentsRequestDto requestDto
) {
ScheduleResponseDto scheduleResponseDto
= scheduleService.updateTitleContents(id, requestDto.getTitle(), requestDto.getContents(), loginMember);
return ResponseEntity.ok(scheduleResponseDto);
}
작성자만 해당 일정을 수정할 수 있도록 만드는 로직을 Service로 옮겨서
Controller는 간결하게 보이도록 했다.
@Transactional
public ScheduleResponseDto updateTitleContents(Long id, String title, String contents, Member loginMember) {
Schedule schedule = scheduleRepository.findById(id).orElseThrow(
() -> new ApplicationException(ErrorMessageCode.NOT_FOUND,
List.of(new ApiError(CustomErrorMessageCode.ID_NOT_FOUND.getCode(),
CustomErrorMessageCode.ID_NOT_FOUND.getMessage())))
);
Long findMemberId = schedule.getMember().getId();
log.info("fineMemberId={}",findMemberId);
log.info("loginMember.getId()={}",loginMember.getId());
// 작성자만 수정 가능
if (loginMember.getId() != findMemberId){
throw new ApplicationException(ErrorMessageCode.FORBIDDEN,
List.of(new ApiError(CustomErrorMessageCode.NOT_OWNER.getCode(),
CustomErrorMessageCode.NOT_OWNER.getMessage())));
}
schedule.updateTitleContents(title, contents);
return new ScheduleResponseDto(
schedule.getId(),
schedule.getTitle(),
schedule.getContents(),
schedule.getMember().getName(),
schedule.getMember().getEmail(),
schedule.getCreatedAt(),
schedule.getModifiedAt()
);
}
- 예외처리 커스텀하기(Enum 사용해서 코드 구성하기)
@Getter
@RequiredArgsConstructor
public enum CustomErrorMessageCode {
ID_NOT_FOUND("ID_NOT_FOUND", "해당 id를 찾을 수 없습니다."),
NOT_OWNER("NOT_OWNER", "본인(작성자)이 아닌 경우 권한이 없습니다."),
INVALID_PASSWORD("INVALID_PASSWORD", "비밀번호가 일치하지 않습니다.");
private final String code;
private final String message;
}
예외처리 커스텀 code를 위와 같이 enum 타입으로 만들어서
아래와 같은 메시지가 보이도록 만들었다.
2025.02.12(수)
일정관리 Develop 주석 작성(Javadoc) 및 리팩토링
Javadoc 이용한 주석 작성
지난 번 과제 때 튜터님께서 Javadoc을 이용해서 주석을 작성하는 것을 추천하셔서 아래와 같이 작성해보았다.
예시)
/**
* 회원 생성(회원가입)
* @param name 회원명
* @param email 이메일
* @param password 비밀번호
* encryptPassword : 비밀번호 암호화 후 저장
* @return SingUpResponseDto 회원 생성 응답 DTO 반환
*/
@Transactional
public SingUpResponseDto save(String name, String email, String password) {
String encryptPassword = passwordEncoder.encode(password); //비밀번호 암호화
Member member = new Member(name, email, encryptPassword);
Member savedMember = memberRepository.save(member);
return new SingUpResponseDto(savedMember.getId(),savedMember.getName(),savedMember.getEmail());
}
리팩토링
- 반복적으로 사용되는 부분을 따로 메서드로 분리하기
예시)
* getMemberByIdOrElseThrow : 회원 id로 회원 조회
: 선택 조회, 수정, 삭제 시 필요한 기능이었는데 이를 메서드로 분리했다.
* validateMemberId : 본인(작성자) 검증 로직
: 선택 수정, 삭제 시 작성자인지 검증하는 로직이 필요했는데 이 부분을 메서드로 분리했다.
/**
* 회원 선택 수정(회원명, 이메일 수정)
* @param id 회원 id
* @param name 회원명
* @param email 이메일
* @param loginMember 세션 로그인 멤버
* getMemberByIdOrElseThrow : 회원 id로 회원 조회
* validateMemberId : 본인(작성자) 검증 로직
* @return MemberResponseDto 회원 응답 DTO 반환
*/
@Transactional
public MemberResponseDto updateNameAndEmail(Long id, String name, String email, Member loginMember) {
Member member = getMemberByIdOrElseThrow(id);
validateMemberId(id, loginMember);
member.updateNameAndEmail(name,email);
return new MemberResponseDto(
member.getId(),
member.getName(),
member.getEmail()
);
}
/**
* 회원 id로 회원 조회
* @param id 회원 id
* @exception ApplicationException 해당 id를 찾을 수 없는 경우, 404 예외처리(커스텀 예외처리 ID_NOT_FOUND)
* @return 회원 id에 해당하는 회원 반환
*/
private Member getMemberByIdOrElseThrow(Long id) {
return memberRepository.findById(id).orElseThrow(
() -> new ApplicationException(ErrorMessageCode.NOT_FOUND,
List.of(new ApiError(CustomErrorMessageCode.ID_NOT_FOUND.getStatus(),
CustomErrorMessageCode.ID_NOT_FOUND.getMessage())))
);
}
/**
* 본인(작성자)만 본인(작성자) 검증 로직
* @param id 회원 id
* @param loginMember 세션 로그인 멤버
* loginMember(로그인 정보)의 id와 DB에서 조회한 id 비교
* @exception ApplicationException 각각의 id가 불일치할 경우 403 예외처리(커스텀 예외처리 NOT_OWNER)
*/
private static void validateMemberId(Long id, Member loginMember) {
if (!loginMember.getId().equals(id)){
throw new ApplicationException(ErrorMessageCode.FORBIDDEN,
List.of(new ApiError(CustomErrorMessageCode.NOT_OWNER.getStatus(),
CustomErrorMessageCode.NOT_OWNER.getMessage())));
}
}
다음에 진행할 때에는 Javadoc을 활용해서 미리 주석도 달아두고,
리팩토링하면 좋을 만한 점들을 미리 생각해두면서 작업하면 좋을 것 같다는 교훈을 얻었다.
'CODING > 스파르타 내일배움캠프 TIL' 카테고리의 다른 글
38_3진법 뒤집기_일정관리 Develop 개인과제 Lv 2,3,4 필수과제 구현완료_25.2.7(금) (0) | 2025.02.07 |
---|---|
37_최대공약수와 최소공배수_일정관리 개인과제 피드백_일정관리 Develop 개인과제 Lv 1, 2 구현중_25.2.6(목) (0) | 2025.02.06 |
36_직사각형 별찍기_스탠다드 트랜잭션, 인덱스_숙련 Spring 3주차_25.2.5(수) (0) | 2025.02.05 |
35_행렬의 덧셈_일정관리 개인과제 해설_숙련 Spring 1,2주차_25.2.4(화) (1) | 2025.02.04 |
34_문자열 다루기 기본_스탠다드 데이터베이스 연관관계 매핑_일정관리 과제 Lv 5 구현완료_25.2.3(월) (0) | 2025.02.03 |