Skip to content
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
29a3411
feat : 일단계 테스트 통과
nonactress Dec 24, 2025
76e20ba
feat(dao) : 이메일로 멤버 찾기
nonactress Dec 24, 2025
6884888
feat(MemberController) : 회원 조회
nonactress Dec 26, 2025
b994a63
feat : 2단계 통과~!!!!
nonactress Dec 26, 2025
9234df4
feat : AdminInterceptor 생성
nonactress Dec 26, 2025
103e6bb
feat(webConfig) : admin인터셉터 추가
nonactress Dec 26, 2025
ecd0c7f
fix : 개행정리
nonactress Dec 26, 2025
334327e
fix : 개행정리2
nonactress Dec 26, 2025
eeb4f66
fix : 패키지 정리
nonactress Dec 26, 2025
a75257b
fix : 1단계 login 쿠키 설정 및 tokenResponse dto 제거
nonactress Dec 26, 2025
f95bdc5
feat : time 엔티티로 수정 및 repository 생성
nonactress Dec 31, 2025
c1610e8
fix(Time) : 서비스에서 timerepository 사용 리팩토링
nonactress Dec 31, 2025
aa8870e
fix(Theme) : 컨트롤러에서 DAO -> Repository 사용
nonactress Dec 31, 2025
9f54ac5
fix(All)
nonactress Jan 1, 2026
a674fa5
fix(All) : reservation.save 오버로딩 및 영속성 관리 하여 에러 수정
nonactress Jan 1, 2026
f9f63d1
refactor(Reservaiton) : 예약 조회 기능 생성
nonactress Jan 1, 2026
5ee3e74
feat : waiting 엔티티 생성
nonactress Jan 1, 2026
cc28fde
feat : WaitingRepository 생성
nonactress Jan 1, 2026
b5cca3b
fix : ReservationService 리팩토링
nonactress Jan 1, 2026
9deeb22
feat : waiting dto 생성
nonactress Jan 1, 2026
7742db7
feat : WaitingService,controller 생성
nonactress Jan 1, 2026
0812bd7
개행정리
nonactress Jan 1, 2026
8f084a7
feat : 예외 처리
nonactress Jan 1, 2026
6d27e4c
refactor : syso -> log.info 로 수정
nonactress Jan 9, 2026
3a50097
refactor : 메소드 delete로 수정
nonactress Jan 9, 2026
4178346
fix : readOnly로 수정
nonactress Jan 9, 2026
dcdd017
fix : 정적 펙토리 메소드를 사용
nonactress Jan 9, 2026
c7a6a64
DEL : 주석 정리
nonactress Jan 9, 2026
4252f96
DEL : import문 제거
nonactress Jan 9, 2026
e948375
fix : 닷 기준 메소드 이쁘게 배치
nonactress Jan 13, 2026
bb539bd
fix : service에서 return dto -> return reservaitonId로 변경
nonactress Jan 13, 2026
c9f6c0d
Update src/main/java/roomescape/member/MemberController.java
nonactress Jan 13, 2026
841a720
fix : 체인닝 메소드 정리
nonactress Jan 13, 2026
47e66f3
fix : dao 파일 정리 및 NPE 방어 코드 작성
nonactress Jan 13, 2026
941299b
fix : 비로그인 login/check 상태코드 401로 수정
nonactress Jan 13, 2026
42fe9b0
refactor : setter 삭제 및 안쓰는 메소드 삭제
nonactress Jan 13, 2026
658a29b
refactor : NotFoundException 적용
nonactress Jan 13, 2026
d10537a
Merge branch 'hyeonjin2' of https://github.com/nonactress/spring-basi…
nonactress Jan 13, 2026
e13ba3b
refactor : 줄바꿈 정리
nonactress Jan 13, 2026
34debcb
refactor : 리포맷 적용
nonactress Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,20 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0'

implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
implementation 'io.jsonwebtoken:jjwt-gson:0.11.2'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
testImplementation 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'




runtimeOnly 'com.h2database:h2'
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/roomescape/advice/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package roomescape.advice;

public record ErrorResponse(String message) {
}
28 changes: 28 additions & 0 deletions src/main/java/roomescape/advice/GlobalExceptionHandler.java
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChangeSetPersister.NotFoundException.class는 처음 보는 예외인데요
요거는 어디서 사용하나요?? 코드에 현진님이 따로 적용한 부분은 없어가지구요

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아.... 이게 자동완성으로 만들어진 것 같습니다!
image

image

여기서 던져지는 예외가 NoSuchElementException인데 notfoundException으로 생각하고 바로 글로벌 핸들러에서 처리하려고 한 것 같습니다! 그래서 위 예외 ChangeSetPersister.NotFoundException.class는 Spring Data 프로젝트의 Cross-Store 기능을 위해 만들어졌다고 합니다!

  • Cross-Store : 하나의 엔티티를 두 가지 이상의 데이터 저장소(예: 관계형 DB인 MySQL + NoSQL인 MongoDB)에 동시에 나누어 관리하는 기술입니다.

예외 상황

  • 데이터 정합성 오류: 여러 DB에서 데이터를 모아 하나의 객체를 완성해야 하는데, 마땅히 있어야 할 특정 데이터가 한쪽 DB에서 사라졌을 때 발생합니다.

  • 상태 동기화 실패: 두 저장소 간의 상태를 동기화하는 도중, 참조해야 할 원본 데이터가 존재하지 않을 때 시스템이 이 예외를 던집니다.


NotFoundException을 만들어서 원래 하려던 방향으로 다시 수정해보겠습니다!

반영 커밋 : 658a29b

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("오류가 발생했습니다."));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 글 참고해서 설정해주시면 좋아요~

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 감사합니다!! 덕분에 편하게 적용했습니다!!

11 changes: 11 additions & 0 deletions src/main/java/roomescape/auth/AuthMember.java
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 {
}
106 changes: 106 additions & 0 deletions src/main/java/roomescape/infrastructure/DataInitializer.java
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("초기 데이터 로딩이 완료되었습니다.");
}
}
42 changes: 42 additions & 0 deletions src/main/java/roomescape/infrastructure/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package roomescape.infrastructure;

import io.jsonwebtoken.*;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기는 이 글 참고하셔서 1번 설정처럼 IntelliJ 내부 설정 값 변경하시면 좋아보여요~

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
Jws<Claims> claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);

사소한거긴한데 .을 기준으로 한줄에 최대한 하나씩 적어두는게 좋습니다요

@nonactress nonactress Jan 13, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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;
}
}
}

24 changes: 24 additions & 0 deletions src/main/java/roomescape/member/Member.java
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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기만 이름을 명시해둔 이유가 있나요?!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

따로 이유는 없고 다른 필드들 nullable처리를 해서 이름도 이렇게 설정하는 구나 해서 넣어봤습니다!

private boolean deleted = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete 종류도 soft delete, hard delete가 있는데, 현진님이 soft delete를 선택한 이유는 무엇인가요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 member가 삭제되더라도 reservation 값들을 가지고 있어야 하는데 만약 hard delete를 이용하면 member가 없어짐에 따라reservation에 있는 member_fk가 가르키는 값이 없어지게 될 수 있으므로 soft delete를 사용했습니다!

@70825 70825 Jan 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러면 reservation 데이터도 같이 없어지면 되는거 아닌가라는 생각이 드네요
그런 의미로 cascade 옵션도 찾아보시면 좋아보입니다 ㅋㅋㅋ (반영은 선택적으로..)

제가 같이 없어지면 되는게 아닌가라고 생각하는 이유
= 사용자가 예약 했다가, 탈퇴하면 예약 정보도 당연히 사라지는게 아닐까라는 생각


protected Member() {
}

public Member(Long id, String name, String email, String role) {
this.id = id;
this.name = name;
Expand Down Expand Up @@ -40,4 +60,8 @@ public String getPassword() {
public String getRole() {
return role;
}

public void delete() {
this.deleted = true;
}
}
30 changes: 29 additions & 1 deletion src/main/java/roomescape/member/MemberController.java
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;

Expand All @@ -19,6 +19,34 @@ public MemberController(MemberService memberService) {
this.memberService = memberService;
}

@PostMapping("/login")
public ResponseEntity<Void> login(
@RequestBody MemberRequest memberRequest,
HttpServletResponse response) {
Comment thread
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기 궁금한 부분이 있는데요
keep-alive 헤더 설정한 이유가 있나요?

@nonactress nonactress Jan 13, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/login/login/check 이후에는 추가적으로 통신하는 작업이 거의 필수적이라고 생각해서 connection을 유지 할 수 있도록 해보았습니다!

.body(new MemberResponse(null, member.getName(), null));
}

@PostMapping("/members")
public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) {
MemberResponse member = memberService.createMember(memberRequest);
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/roomescape/member/MemberDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ public Member findByEmailAndPassword(String email, String password) {
);
}

public Member findByName(String name) {
public Member findByEmail(String email) {
return jdbcTemplate.queryForObject(
"SELECT id, name, email, role FROM member WHERE name = ?",
"SELECT id, name, email, role FROM member WHERE email = ?",
(rs, rowNum) -> new Member(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email"),
rs.getString("role")
),
name
email
);
}
}
}
55 changes: 55 additions & 0 deletions src/main/java/roomescape/member/MemberRepository.java
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클래스에 @Transactional(readOnly = true)로 선언한 이유는 무엇인가요??
왜냐하면 클래스에 @Transactional를 설정할 수도 있어보이는데 readOnly 옵션을 적용한걸 선택하셔서요

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다양한 코드를 적용 시켜보고 싶어서 readOnly= ture 를 사용해 보았습니다!!
따로 비약적인 성능의 개선이나 로직의 가독성의 향상은 없지만 스냅샷을 체크하지 않는 find 와 관련된 메소드들에 좋을 것 같아 적용시켜 봤습니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러면 클래스 위에 @Transactional(readOnly=true)로 설정하게 될 경우의 장단점은 뭐가 있다고 생각하시나요??

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

장점


  1. dirtyChecking을 하지 않아 snapShot을 가지고 있지 않아 메모리 관리에 효율적입니다!
  2. db 수정이 없다는 것이 명시됨으로 가독성이 올라갑니다!

단점


  1. 클래스 설정으로 transactional(readOnly=true) 한 것을 보지 못하고 cud 작업을 하게 될 수 도 있습니다!

추가

실무에선 두개의 db를 이용한다...??

Master DB: 쓰기(Insert/Update/Delete) 전용
Slave DB: 읽기(Select) 전용

readOnly=true가 설정된 메서드는 알아서 Slave DB로 접속하게끔 인프라를 구성할 수 있어, 전체 시스템의 부하를 획기적으로 줄일 수 있다고 하는데 맞는지 궁금합니다!

@70825 70825 Jan 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍 단점의 경우 좀 크리티컬 할 수 있는데요. CUD 작업을 서버 배포하고 나서야 알 수 있기 때문에 클래스단에 @Transactional 을 적용하는 방법도 있을 것 같아요. 저는 팀에서 메서드 위에 @Transactional, @Transactional(readOnly=true)를 붙이는게 암묵적 컨벤션이더라구요

실무에선 두개의 db를 이용한다...??
Master DB: 쓰기(Insert/Update/Delete) 전용
Slave DB: 읽기(Select) 전용
readOnly=true가 설정된 메서드는 알아서 Slave DB로 접속하게끔 인프라를 구성할 수 있어, 전체 시스템의 부하를 획기적으로 줄일 수 있다고 하는데 맞는지 궁금합니다!

알아서 접속하게 만드려면 코드단에서 AbstractRoutingDataSource를 구현해서 readOnly를 사용하는지 여부를 파악하는 메서드를 사용해야 readOnly true/false 옵션에 맞게 접속가능해요

보통 책에서는 읽기 요청 : 쓰기 요청 비율이 8:2, 9:1인 편이라고 하는데요
장점은 현진님 말씀처럼 부하도 분산할 수 있고, Master는 CUD 연산에만 집중할 수도 있긴해요
그리고 Master DB가 다운되어도 MMM 구조로 만들어져 있으면 Slave가 Master로 승격되어서 안전한 구조라고 알고 있어요

다만 단점은 Master에서 작업한 CUD도 결국 Slave로 반영해야하는데, 이때 비동기로 전달되어서 읽기 요청을 했는데 아직 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);
}
}
2 changes: 1 addition & 1 deletion src/main/java/roomescape/member/MemberRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ public String getEmail() {
public String getPassword() {
return password;
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 아직 적용이 안된 것 같은데 확인 부탁드려요~

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

설정은 해놨는데 적용이 안되었네요ㅠㅠㅠ
다시 넣어보겠습니다!!

반영 커밋 : e13ba3b

Loading