Skip to content

Commit d45a9d8

Browse files
authored
Merge pull request #30 from Immmii/master
[22기_이수아] 동시성 & 결제 연동 미션 제출합니다.
2 parents d5ec7d4 + db64a04 commit d45a9d8

File tree

101 files changed

+1388
-638
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+1388
-638
lines changed

README.md

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,4 +292,94 @@ Access Token의 유효기간을 짧게 하면 로그인을 자주해야해서
292292
**장단점**
293293

294294
- (장) 소셜 계정으로 빠른 온보딩, 외부 자원 접근 위임 표준.
295-
- (단) 리다이렉트/코드 교환 등 플로우 복잡, 제공자별 설정·검증 필요.
295+
- (단) 리다이렉트/코드 교환 등 플로우 복잡, 제공자별 설정·검증 필요.
296+
297+
298+
## 코드 리팩토링 (책임 분리)
299+
참고)
300+
https://youtu.be/dJ5C4qRqAgA?si=WAgUBNGA9G_B8Vl0
301+
302+
![Image](https://github.com/user-attachments/assets/519b2c85-22ac-48a1-a2da-5651278bc809)
303+
304+
![Image](https://github.com/user-attachments/assets/a7206b5d-1279-42bd-be96-81907555bb2b)
305+
306+
![Image](https://github.com/user-attachments/assets/18e4266f-835c-4e4f-add3-f0a29a280ab6)
307+
308+
![Image](https://github.com/user-attachments/assets/a8096df5-93f0-4eba-88b5-9720ca202d9a)
309+
310+
객체 참조는 결합도가 가장 높은 의존성 -> 객체 참조를 끊어 결합도를 낮추자!
311+
312+
![Image](https://github.com/user-attachments/assets/88998601-3531-4993-8a00-ba01c65221cd)
313+
314+
![Image](https://github.com/user-attachments/assets/17161707-3a10-4150-98bd-dfc0146a82f2)
315+
316+
317+
1. 의존성 설계 수정
318+
@ManyToOne, @OneToMany 관계로 풀어낸 Entity 관계들 중
319+
결합도가 높을 필요가 없는 경우, Entity 참조 -> id 참조할 수 있도록 Entity 설계 수정
320+
321+
2. Service layer 책임 분리
322+
ex) Member Domain Service
323+
```
324+
@Service
325+
@RequiredArgsConstructor
326+
public class MemberReader {
327+
328+
private final MemberRepository memberRepository;
329+
330+
/** 회원 단건 조회 */
331+
public Member getById(Long memberId) {
332+
MemberEntity member = memberRepository.findById(memberId)
333+
.orElseThrow(() -> new IllegalArgumentException("회원을 찾을 수 없습니다. id=" + memberId));
334+
return Member.from(member);
335+
}
336+
}
337+
```
338+
339+
```
340+
@Service
341+
@RequiredArgsConstructor
342+
public class MemberSaver {
343+
344+
private final MemberRepository memberRepository;
345+
346+
/** 회원 가입 */
347+
@Transactional
348+
public Long execute(CreateMemberCommand createMemberCommand) { // member join command
349+
MemberEntity saved = memberRepository.save(createMemberCommand.toEntity());
350+
return saved.getId();
351+
}
352+
353+
}
354+
```
355+
356+
```
357+
@Service
358+
@RequiredArgsConstructor
359+
public class AuthService {
360+
361+
private final MemberReader memberReader;
362+
private final MemberSaver memberSaver;
363+
private final PasswordEncoder passwordEncoder; // BCryptPasswordEncoder
364+
365+
@Transactional
366+
public void signUp(SignUpRequest req) {
367+
// 이미 존재하는 loginId 체크
368+
memberReader.getByLoginId(req.loginId());
369+
370+
// 새 회원 생성 (비밀번호는 반드시 암호화)
371+
CreateMemberCommand m = new CreateMemberCommand(
372+
req.name(),
373+
req.age(),
374+
req.gender(),
375+
req.loginId(),
376+
passwordEncoder.encode(req.password())
377+
);
378+
379+
memberSaver.execute(m);
380+
381+
}
382+
}
383+
```
384+
385+
Controller가 참조하는 Service의 Repository 의존성을 없앨 수 있다!

cgv_clone/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ out/
3737
.vscode/
3838

3939
### env ###
40-
.env
40+
src/main/resources/local.env

cgv_clone/build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ dependencies {
3737
annotationProcessor 'org.projectlombok:lombok'
3838

3939
// swagger
40-
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
40+
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
4141

4242
// database
4343
// runtimeOnly 'com.h2database:h2'
@@ -53,6 +53,9 @@ dependencies {
5353
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
5454
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
5555

56+
//webclient
57+
implementation 'org.springframework.boot:spring-boot-starter-webflux'
58+
5659
// test
5760
testImplementation 'org.springframework.boot:spring-boot-starter-test'
5861
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//package com.ceos22.cgv_clone;
2+
//
3+
//import com.ceos22.cgv_clone.domain.reservationMovie.Movie;
4+
//import com.ceos22.cgv_clone.repository.MovieRepository;
5+
//import jakarta.transaction.Transactional;
6+
//import lombok.RequiredArgsConstructor;
7+
//import org.springframework.boot.CommandLineRunner;
8+
//import org.springframework.stereotype.Component;
9+
//
10+
//import java.util.List;
11+
//
12+
//@Component
13+
//@RequiredArgsConstructor
14+
//public class DevDataSeeder implements CommandLineRunner {
15+
//
16+
// private final MovieRepository movieRepository;
17+
//
18+
// @Override
19+
// @Transactional
20+
// public void run(String... args) {
21+
// if (movieRepository.count() > 0) return; // 중복 삽입 방지
22+
//
23+
// List<Movie> movies = List.of(
24+
// new Movie("서울의 봄", 140, "1979년 12월, 계엄 하의 정치 격변 속 실제 사건을 모티브로 한 드라마."),
25+
// new Movie("오펜하이머", 180, "핵개발 프로젝트의 딜레마와 인물의 내면을 그린 전기 드라마."),
26+
// new Movie("듄: 파트2", 166, "사막 행성 아라키스에서 펼쳐지는 권력과 예언의 서사."),
27+
// new Movie("인사이드 아웃 2", 96, "사춘기 감정들이 늘어나며 벌어지는 좌충우돌 성장기."),
28+
// new Movie("콘크리트 유토피아", 130, "재난 이후 한 아파트에 모인 생존자들의 이야기."),
29+
// new Movie("범죄도시 4", 109, "형사 마석도의 거침없는 범죄 소탕 작전."),
30+
// new Movie("탑건: 매버릭", 131, "전설의 파일럿이 귀환해 새로운 팀을 이끄는 항공 액션."),
31+
// new Movie("어벤져스: 엔드게임", 181, "인피니티 사가의 대미를 장식하는 히어로 대서사."),
32+
// new Movie("라라랜드", 128, "꿈을 좇는 두 청춘의 사랑과 선택을 담은 뮤지컬 드라마."),
33+
// new Movie("기생충", 132, "두 가족의 얽힘으로 드러나는 계급의 아이러니."),
34+
// new Movie("인터스텔라", 169, "인류의 미래를 위한 우주 탐사와 시간의 상대성."),
35+
// new Movie("소울", 100, "음악가의 영혼 여행을 통해 삶의 의미를 묻는 애니메이션.")
36+
// );
37+
//
38+
// movieRepository.saveAll(movies);
39+
// }
40+
//}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.ceos22.cgv_clone;
2+
3+
import org.springframework.core.annotation.AliasFor;
4+
import org.springframework.stereotype.Component;
5+
6+
import java.lang.annotation.*;
7+
8+
@Target(ElementType.TYPE)
9+
@Retention(RetentionPolicy.RUNTIME)
10+
@Documented
11+
@Component
12+
public @interface UseCase {
13+
14+
/**
15+
* Alias for {@link Component#value}.
16+
*/
17+
@AliasFor(annotation = Component.class)
18+
String value() default "";
19+
20+
}

cgv_clone/src/main/java/com/ceos22/cgv_clone/SecurityConfig.java renamed to cgv_clone/src/main/java/com/ceos22/cgv_clone/api/config/SecurityConfig.java

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
package com.ceos22.cgv_clone;
1+
package com.ceos22.cgv_clone.api.config;
22

33
import com.ceos22.cgv_clone.security.JwtAuthenticationFilter;
44
import lombok.RequiredArgsConstructor;
55
import org.springframework.context.annotation.Bean;
66
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.http.HttpMethod;
78
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
89
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
10+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
911
import org.springframework.security.config.http.SessionCreationPolicy;
1012
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
1113
import org.springframework.security.crypto.password.PasswordEncoder;
@@ -19,10 +21,8 @@ public class SecurityConfig {
1921

2022
private final JwtAuthenticationFilter jwtFilter;
2123

22-
private static final String[] SWAGGER_WHITELIST = {
23-
"/v3/api-docs/**",
24-
"/swagger-ui/**",
25-
"/swagger-ui.html"
24+
private static final String[] SWAGGER = {
25+
"/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html"
2626
};
2727

2828
@Bean
@@ -34,18 +34,19 @@ public PasswordEncoder passwordEncoder() {
3434
@Bean
3535
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
3636
return http
37-
// (1) csrf - swagger 경로 예외
38-
.csrf(csrf -> csrf
39-
.ignoringRequestMatchers(SWAGGER_WHITELIST) // swagger 경로 예외
40-
)
37+
// (1) csrf
38+
.csrf(AbstractHttpConfigurer::disable)
4139

4240
// (2) 세션
4341
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
4442

4543
// (3) 인증 / 인가
4644
.authorizeHttpRequests(reg -> reg
47-
.requestMatchers(SWAGGER_WHITELIST).permitAll() // Swagger 허용
48-
.requestMatchers("/api/auth/**").permitAll() // 로그인/회원가입
45+
.requestMatchers(SWAGGER).permitAll() // Swagger 허용
46+
.requestMatchers(HttpMethod.POST,"/api/auth/**").permitAll() // 로그인/회원가입
47+
.requestMatchers("/api/reservation/**",
48+
"/api/favorites/movies/*/*/toggle",
49+
"/api/favorites/cinemas/*/*/toggle").permitAll()
4950
.anyRequest().authenticated() // 나머지 인증
5051
)
5152

cgv_clone/src/main/java/com/ceos22/cgv_clone/SwaggerConfig.java renamed to cgv_clone/src/main/java/com/ceos22/cgv_clone/api/config/SwaggerConfig.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.ceos22.cgv_clone;
1+
package com.ceos22.cgv_clone.api.config;
22

33

44
import io.swagger.v3.oas.models.Components;
@@ -21,8 +21,8 @@ public OpenAPI customOpenAPI() {
2121

2222
private Info apiInfo() {
2323
return new Info()
24-
.title("Spring RESTful API")
25-
.description("RESTful API reference for developers")
24+
.title("Cgv-clone API")
25+
.description("CEOS-Developers / Immmii / spring-cgv-22nd")
2626
.version("1.0.0");
2727
}
2828
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.ceos22.cgv_clone.api.config;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.http.HttpHeaders;
7+
import org.springframework.web.reactive.function.client.WebClient;
8+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
9+
10+
@Configuration
11+
public class WebClientConfig implements WebMvcConfigurer {
12+
13+
@Value("${payment.base-url}")
14+
private String baseUrl;
15+
16+
@Value("${payment.api-secret}")
17+
private String apiSecretKey;
18+
19+
@Bean
20+
public WebClient paymentWebClient() {
21+
return WebClient.builder()
22+
.baseUrl(baseUrl)
23+
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiSecretKey)
24+
.build();
25+
}
26+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.ceos22.cgv_clone.api.config.response;
2+
3+
import com.ceos22.cgv_clone.common.dto.ErrorResponse;
4+
import com.ceos22.cgv_clone.common.dto.ErrorReason;
5+
import com.ceos22.cgv_clone.common.exception.BaseErrorCode;
6+
import com.ceos22.cgv_clone.common.exception.CgvCloneBusinessException;
7+
import jakarta.servlet.http.HttpServletRequest;
8+
import org.springframework.http.HttpStatus;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.web.bind.MethodArgumentNotValidException;
11+
import org.springframework.web.bind.annotation.ExceptionHandler;
12+
import org.springframework.web.bind.annotation.RestControllerAdvice;
13+
14+
@RestControllerAdvice
15+
public class GlobalExceptionHandler {
16+
17+
18+
@ExceptionHandler(MethodArgumentNotValidException.class)
19+
protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
20+
System.out.println("MethodArgumentNotValidException 에러를 캐치");
21+
22+
return null; // 수정해야
23+
}
24+
25+
@ExceptionHandler(CgvCloneBusinessException.class)
26+
public ResponseEntity<ErrorResponse> handleCgvCloneBusinessException(
27+
CgvCloneBusinessException e, HttpServletRequest request) {
28+
BaseErrorCode code = e.getErrorCode();
29+
ErrorReason errorReason = code.getErrorReason();
30+
ErrorResponse errorResponse =
31+
new ErrorResponse(errorReason, request.getRequestURL().toString());
32+
return ResponseEntity.status(HttpStatus.valueOf(errorReason.getStatus()))
33+
.body(errorResponse);
34+
}
35+
}

cgv_clone/src/main/java/com/ceos22/cgv_clone/api/AuthController.java renamed to cgv_clone/src/main/java/com/ceos22/cgv_clone/api/controller/AuthController.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
package com.ceos22.cgv_clone.api;
1+
package com.ceos22.cgv_clone.api.controller;
22

3-
import com.ceos22.cgv_clone.domain.dto.LoginReq;
4-
import com.ceos22.cgv_clone.domain.dto.LoginRes;
5-
import com.ceos22.cgv_clone.domain.dto.SignUpReq;
3+
import com.ceos22.cgv_clone.api.dto.LoginRequest;
4+
import com.ceos22.cgv_clone.api.dto.LoginResponse;
5+
import com.ceos22.cgv_clone.api.dto.SignUpRequest;
66
import com.ceos22.cgv_clone.service.AuthService;
77
import com.ceos22.cgv_clone.service.LoginService;
88
import lombok.RequiredArgsConstructor;
@@ -19,12 +19,12 @@ public class AuthController {
1919
private final LoginService loginService;
2020

2121
@PostMapping("/signup")
22-
public void signUp(@RequestBody SignUpReq req) {
22+
public void signUp(@RequestBody SignUpRequest req) {
2323
authService.signUp(req);
2424
}
2525

2626
@PostMapping("/login")
27-
public LoginRes login(@RequestBody LoginReq req) {
27+
public LoginResponse login(@RequestBody LoginRequest req) {
2828
return loginService.login(req);
2929
}
3030
}

0 commit comments

Comments
 (0)