-
Notifications
You must be signed in to change notification settings - Fork 97
[그리디] 서현진 Spring JPA (1차) 4,5,6 단계 미션 제출합니다. #200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: nonactress
Are you sure you want to change the base?
Changes from 29 commits
29a3411
76e20ba
6884888
b994a63
9234df4
103e6bb
ecd0c7f
334327e
eeb4f66
a75257b
f95bdc5
c1610e8
aa8870e
9f54ac5
a674fa5
f9f63d1
5ee3e74
cc28fde
b5cca3b
9deeb22
7742db7
0812bd7
8f084a7
6d27e4c
3a50097
4178346
dcdd017
c7a6a64
4252f96
e948375
bb539bd
c9f6c0d
841a720
47e66f3
941299b
42fe9b0
658a29b
d10537a
e13ba3b
34debcb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| package roomescape.advice; | ||
|
|
||
| public record ErrorResponse(String message) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package roomescape.advice; | ||
|
|
||
| import org.springframework.data.crossstore.ChangeSetPersister; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.ControllerAdvice; | ||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||
|
|
||
| @ControllerAdvice | ||
| public class GlobalExceptionHandler { | ||
|
|
||
| @ExceptionHandler(IllegalArgumentException.class) | ||
| public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException e) { | ||
| return ResponseEntity.badRequest() | ||
| .body(new ErrorResponse(e.getMessage())); | ||
| } | ||
|
|
||
| @ExceptionHandler(ChangeSetPersister.NotFoundException.class) | ||
| public ResponseEntity<ErrorResponse> handleNotFoundException(RuntimeException e) { | ||
| return ResponseEntity.status(404) | ||
| .body(new ErrorResponse(e.getMessage())); | ||
| } | ||
|
|
||
| @ExceptionHandler(Exception.class) | ||
| public ResponseEntity<ErrorResponse> handleException(Exception e) { | ||
| return ResponseEntity.internalServerError() | ||
| .body(new ErrorResponse("오류가 발생했습니다.")); | ||
| } | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기 글 참고해서 설정해주시면 좋아요~
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 감사합니다!! 덕분에 편하게 적용했습니다!! |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package roomescape.auth; | ||
|
|
||
| import java.lang.annotation.ElementType; | ||
| import java.lang.annotation.Retention; | ||
| import java.lang.annotation.RetentionPolicy; | ||
| import java.lang.annotation.Target; | ||
|
|
||
| @Target(ElementType.PARAMETER) | ||
| @Retention(RetentionPolicy.RUNTIME) | ||
| public @interface AuthMember { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package roomescape.infrastructure; | ||
|
|
||
| import org.springframework.boot.CommandLineRunner; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
| import roomescape.member.Member; | ||
| import roomescape.member.MemberRepository; | ||
| import roomescape.theme.Theme; | ||
| import roomescape.theme.ThemeRepository; | ||
| import roomescape.time.Time; | ||
| import roomescape.time.TimeRepository; | ||
| import roomescape.reservation.Reservation; | ||
| import roomescape.reservation.ReservationRepository; | ||
|
|
||
| @Component | ||
| public class DataInitializer implements CommandLineRunner { | ||
|
|
||
| private final MemberRepository memberRepository; | ||
| private final ThemeRepository themeRepository; | ||
| private final TimeRepository timeRepository; | ||
| private final ReservationRepository reservationRepository; | ||
| private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DataInitializer.class); | ||
|
|
||
| public DataInitializer( | ||
| MemberRepository memberRepository, | ||
| ThemeRepository themeRepository, | ||
| TimeRepository timeRepository, | ||
| ReservationRepository reservationRepository | ||
| ) { | ||
| this.memberRepository = memberRepository; | ||
| this.themeRepository = themeRepository; | ||
| this.timeRepository = timeRepository; | ||
| this.reservationRepository = reservationRepository; | ||
| } | ||
|
|
||
| @Override | ||
| @Transactional | ||
| public void run(String... args) throws Exception { | ||
| // 1. 관리자 계정 생성 | ||
| if (memberRepository.findByEmail("admin").isEmpty()) { | ||
| Member admin = new Member("admin", "admin", "admin", "ADMIN"); | ||
| Member user = new Member("user", "user", "user", "USER"); | ||
| memberRepository.save(admin); | ||
| memberRepository.save(user); | ||
| log.info("관리자 계정이 생성되었습니다."); | ||
| } | ||
|
|
||
| // 2. 테마 데이터 생성 | ||
| if (themeRepository.count() == 0) { | ||
| Theme theme1 = new Theme("테마1", "테마1입니다."); | ||
| Theme theme2 = new Theme("테마2", "테마2입니다."); | ||
| Theme theme3 = new Theme("테마3", "테마3입니다."); | ||
|
|
||
| themeRepository.save(theme1); | ||
| themeRepository.save(theme2); | ||
| themeRepository.save(theme3); | ||
| log.info("테마 데이터가 생성되었습니다."); | ||
| } | ||
|
|
||
| // 3. 시간 데이터 생성 | ||
| if (timeRepository.count() == 0) { | ||
| Time time1 = new Time("10:00"); | ||
| Time time2 = new Time("12:00"); | ||
| Time time3 = new Time("14:00"); | ||
| Time time4 = new Time("16:00"); | ||
| Time time5 = new Time("18:00"); | ||
| Time time6 = new Time("20:00"); | ||
|
|
||
| timeRepository.save(time1); | ||
| timeRepository.save(time2); | ||
| timeRepository.save(time3); | ||
| timeRepository.save(time4); | ||
| timeRepository.save(time5); | ||
| timeRepository.save(time6); | ||
| log.info("시간 데이터가 생성되었습니다."); | ||
| } | ||
|
|
||
| if (reservationRepository.count() == 0) { | ||
| Member admin = memberRepository.findByEmail("admin") | ||
| .orElseThrow(() -> new RuntimeException("Admin not found")); | ||
|
|
||
| Time time1 = timeRepository.findById(1L).orElseThrow(); | ||
| Time time2 = timeRepository.findById(2L).orElseThrow(); | ||
| Time time3 = timeRepository.findById(3L).orElseThrow(); | ||
|
|
||
| Theme theme1 = themeRepository.findById(1L).orElseThrow(); | ||
| Theme theme2 = themeRepository.findById(2L).orElseThrow(); | ||
| Theme theme3 = themeRepository.findById(3L).orElseThrow(); | ||
|
|
||
| Reservation reservation1 = new Reservation("", "2024-03-01", time1, theme1, admin); | ||
| Reservation reservation2 = new Reservation("", "2024-03-01", time2, theme2, admin); | ||
| Reservation reservation3 = new Reservation("", "2024-03-01", time3, theme3, admin); | ||
|
|
||
| reservationRepository.save(reservation1); | ||
| reservationRepository.save(reservation2); | ||
| reservationRepository.save(reservation3); | ||
|
|
||
| Reservation reservation4 = new Reservation("브라운", "2024-03-01", time1, theme2); | ||
|
|
||
| reservationRepository.save(reservation4); | ||
| log.info("예약 데이터가 생성되었습니다."); | ||
| } | ||
|
|
||
| log.info("초기 데이터 로딩이 완료되었습니다."); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||
| package roomescape.infrastructure; | ||||||||||
|
|
||||||||||
| import io.jsonwebtoken.*; | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기는 이 글 참고하셔서 1번 설정처럼 IntelliJ 내부 설정 값 변경하시면 좋아보여요~
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵!! 덕분에 개발 환경이 쾌적해 지는 것 같습니다~~!!! 감사합니다ㅏㅏㅏ |
||||||||||
| import org.springframework.beans.factory.annotation.Value; | ||||||||||
| import org.springframework.stereotype.Component; | ||||||||||
|
|
||||||||||
| import java.util.Date; | ||||||||||
|
|
||||||||||
| @Component | ||||||||||
| public class JwtTokenProvider { | ||||||||||
| @Value("${security.jwt.token.secret-key:this-is-a-sample-secret-key-at-least-32-bytes-long}") | ||||||||||
| private String secretKey; | ||||||||||
| @Value("${security.jwt.token.expire-length:3600000}") | ||||||||||
| private long validityInMilliseconds; | ||||||||||
|
|
||||||||||
| public String createToken(String payload) { | ||||||||||
| Claims claims = Jwts.claims().setSubject(payload); | ||||||||||
| Date now = new Date(); | ||||||||||
| Date validity = new Date(now.getTime() + validityInMilliseconds); | ||||||||||
|
|
||||||||||
| return Jwts.builder() | ||||||||||
| .setClaims(claims) | ||||||||||
| .setIssuedAt(now) | ||||||||||
| .setExpiration(validity) | ||||||||||
| .signWith(SignatureAlgorithm.HS256, secretKey) | ||||||||||
| .compact(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| public String getPayload(String token) { | ||||||||||
| return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| public boolean validateToken(String token) { | ||||||||||
| try { | ||||||||||
| Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
사소한거긴한데
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 메소드가 별로 길지 않은 것 같아 그냥 적었는데 확실히 반영 커밋 : e948375 |
||||||||||
| return !claims.getBody().getExpiration().before(new Date()); | ||||||||||
| } catch (JwtException | IllegalArgumentException e) { | ||||||||||
| return false; | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,32 @@ | ||
| package roomescape.member; | ||
|
|
||
| import jakarta.persistence.*; | ||
|
|
||
| @Entity | ||
| @Table(name = "member") | ||
| public class Member { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(nullable = false) | ||
| private String name; | ||
|
|
||
| @Column(nullable = false, unique = true) | ||
| private String email; | ||
|
|
||
| @Column(nullable = false) | ||
| private String password; | ||
|
|
||
| @Column(nullable = false) | ||
| private String role; | ||
|
|
||
| @Column(name = "deleted") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기만 이름을 명시해둔 이유가 있나요?!
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 따로 이유는 없고 다른 필드들 nullable처리를 해서 이름도 이렇게 설정하는 구나 해서 넣어봤습니다! |
||
| private boolean deleted = false; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. delete 종류도 soft delete, hard delete가 있는데, 현진님이 soft delete를 선택한 이유는 무엇인가요?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 만약 member가 삭제되더라도 reservation 값들을 가지고 있어야 하는데 만약 hard delete를 이용하면 member가 없어짐에 따라reservation에 있는 member_fk가 가르키는 값이 없어지게 될 수 있으므로 soft delete를 사용했습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그러면 reservation 데이터도 같이 없어지면 되는거 아닌가라는 생각이 드네요 제가 같이 없어지면 되는게 아닌가라고 생각하는 이유 |
||
|
|
||
| protected Member() { | ||
| } | ||
|
|
||
| public Member(Long id, String name, String email, String role) { | ||
| this.id = id; | ||
| this.name = name; | ||
|
|
@@ -40,4 +60,8 @@ public String getPassword() { | |
| public String getRole() { | ||
| return role; | ||
| } | ||
|
|
||
| public void delete() { | ||
| this.deleted = true; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,13 @@ | ||
| package roomescape.member; | ||
|
|
||
| import jakarta.servlet.http.Cookie; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
| import roomescape.auth.AuthMember; | ||
|
|
||
| import java.net.URI; | ||
|
|
||
|
|
@@ -19,6 +19,34 @@ public MemberController(MemberService memberService) { | |
| this.memberService = memberService; | ||
| } | ||
|
|
||
| @PostMapping("/login") | ||
| public ResponseEntity<Void> login( | ||
| @RequestBody MemberRequest memberRequest, | ||
| HttpServletResponse response) { | ||
|
nonactress marked this conversation as resolved.
Outdated
|
||
|
|
||
| String tokenValue = memberService.login(memberRequest); | ||
|
|
||
| Cookie cookie = new Cookie("token", tokenValue); | ||
| cookie.setHttpOnly(true); | ||
| cookie.setPath("/"); | ||
| cookie.setMaxAge(3600); | ||
| response.addCookie(cookie); | ||
|
|
||
| return ResponseEntity.ok() | ||
| .header("Keep-Alive", "timeout=60") | ||
| .build(); | ||
| } | ||
|
|
||
| @GetMapping("/login/check") | ||
| public ResponseEntity<MemberResponse> check( | ||
| @AuthMember Member member | ||
| ) { | ||
| return ResponseEntity.ok() | ||
| .header("Connection", "keep-alive") | ||
| .header("Keep-Alive", "timeout=60") | ||
|
Comment on lines
+51
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요기 궁금한 부분이 있는데요
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| .body(new MemberResponse(null, member.getName(), null)); | ||
| } | ||
|
|
||
| @PostMapping("/members") | ||
| public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { | ||
| MemberResponse member = memberService.createMember(memberRequest); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package roomescape.member; | ||
|
|
||
| import jakarta.persistence.EntityManager; | ||
| import jakarta.persistence.NoResultException; | ||
| import jakarta.persistence.PersistenceContext; | ||
| import org.springframework.stereotype.Repository; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| @Repository | ||
| @Transactional(readOnly = true) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 클래스에
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다양한 코드를 적용 시켜보고 싶어서 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그러면 클래스 위에
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 장점
단점
추가실무에선 두개의 db를 이용한다...??Master DB: 쓰기(Insert/Update/Delete) 전용 readOnly=true가 설정된 메서드는 알아서 Slave DB로 접속하게끔 인프라를 구성할 수 있어, 전체 시스템의 부하를 획기적으로 줄일 수 있다고 하는데 맞는지 궁금합니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍👍 단점의 경우 좀 크리티컬 할 수 있는데요. CUD 작업을 서버 배포하고 나서야 알 수 있기 때문에 클래스단에
알아서 접속하게 만드려면 코드단에서 AbstractRoutingDataSource를 구현해서 readOnly를 사용하는지 여부를 파악하는 메서드를 사용해야 readOnly true/false 옵션에 맞게 접속가능해요 보통 책에서는 읽기 요청 : 쓰기 요청 비율이 8:2, 9:1인 편이라고 하는데요 다만 단점은 Master에서 작업한 CUD도 결국 Slave로 반영해야하는데, 이때 비동기로 전달되어서 |
||
| public class MemberRepository { | ||
|
|
||
| @PersistenceContext | ||
| private EntityManager em; | ||
|
|
||
| @Transactional | ||
| public Member save(Member member) { | ||
| em.persist(member); | ||
| return member; | ||
| } | ||
|
|
||
| public Optional<Member> findByEmailAndPassword(String email, String password) { | ||
| try { | ||
| Member member = em.createQuery( | ||
| "SELECT m FROM Member m WHERE m.email = :email AND m.password = :password", | ||
| Member.class) | ||
| .setParameter("email", email) | ||
| .setParameter("password", password) | ||
| .getSingleResult(); | ||
| return Optional.of(member); | ||
| } catch (NoResultException e) { | ||
| return Optional.empty(); | ||
| } | ||
| } | ||
|
|
||
| public Optional<Member> findByEmail(String email) { | ||
| try { | ||
| Member member = em.createQuery( | ||
| "SELECT m FROM Member m WHERE m.email = :email", | ||
| Member.class) | ||
| .setParameter("email", email) | ||
| .getSingleResult(); | ||
| return Optional.of(member); | ||
| } catch (NoResultException e) { | ||
| return Optional.empty(); | ||
| } | ||
| } | ||
|
|
||
| public Optional<Member> findById(Long id) { | ||
| Member member = em.find(Member.class, id); | ||
| return Optional.ofNullable(member); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,4 +16,4 @@ public String getEmail() { | |
| public String getPassword() { | ||
| return password; | ||
| } | ||
| } | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기 아직 적용이 안된 것 같은데 확인 부탁드려요~
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ChangeSetPersister.NotFoundException.class는 처음 보는 예외인데요요거는 어디서 사용하나요?? 코드에 현진님이 따로 적용한 부분은 없어가지구요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아.... 이게 자동완성으로 만들어진 것 같습니다!

여기서 던져지는 예외가 NoSuchElementException인데 notfoundException으로 생각하고 바로 글로벌 핸들러에서 처리하려고 한 것 같습니다! 그래서 위 예외
ChangeSetPersister.NotFoundException.class는 Spring Data 프로젝트의 Cross-Store 기능을 위해 만들어졌다고 합니다!예외 상황
데이터 정합성 오류: 여러 DB에서 데이터를 모아 하나의 객체를 완성해야 하는데, 마땅히 있어야 할 특정 데이터가 한쪽 DB에서 사라졌을 때 발생합니다.
상태 동기화 실패: 두 저장소 간의 상태를 동기화하는 도중, 참조해야 할 원본 데이터가 존재하지 않을 때 시스템이 이 예외를 던집니다.
NotFoundException을 만들어서 원래 하려던 방향으로 다시 수정해보겠습니다!반영 커밋 : 658a29b