Загружаем каталог…
Загружаем каталог…
MSA에서는 사용자, 상품, 주문 서비스가 게이트웨이의 경로로 구분되어 호출되며, 각 서비스는 자신의 데이터베이스만 조회한다. 따라서 서비스끼리 연동하기 전에는 주문이 저장되어 있어도 사용자 상세 조회의 주문 내역이 빈 리스트로 응답된다. 📑 목차 🔑 상황별 문법 찾기 📖 오늘의 주요 학습 내용 🚨 막혔던 지점 🎯 다음에 할 일 🌱 오늘 성장 기록 🔑 상황별 문법 찾기 이런 상황 쓰는 문법 이해도 게이트웨이에서 경로로 서비스를 나눌 때 uri: lb://USER-SERVICE + Path=/user-service/** ✅ 요청 객체의 값을 다른 객체로 옮겨 담을 때 mapper.map(user, UserDto.class) ✅ 필드 이름이 정확히 같을 때만 옮기게 할 때 setMatchingStrategy(MatchingStrategies.STRICT) 🔺 외부에 보여 줄 식별자를 만들 때 UUID.randomUUID().toString() ✅ 비밀번호를 암호화해서 저장할 때 passwordEncoder.encode(userDto.getPwd()) ✅ 등록 성공을 201로 응답할 때 ResponseEntity.status(HttpStatus.CREATED).body(responseUser) ✅ 값이 없는 필드를 응답 JSON에서 뺄 때 @JsonInclude(JsonInclude.Include.NON_NULL) ✅ 컬럼 값으로 조회하는 메서드를 만들 때 UserEntity findByUserId(String userId); ✅ 경로에 들어 있는 값을 인자로 받을 때 @PathVariable("userId") String userId ✅ 특정 경로만 인증 없이 열어 둘 때 .requestMatchers("/h2-console/**").permitAll() 🔺 나머지 요청에 Basic 인증을 요구할 때 .anyRequest().authenticated() + .httpBasic(Customizer.withDefaults()) ✅ 서버가 기동할 때 초기 데이터를 넣을 때 data.sql + defer-datasource-initialization: true ✅ 생성 시각을 DB 기본값으로 채울 때 @ColumnDefault(value = "CURRENT_TIMESTAMP") + insertable = false 🔺 브라우저에서 H2 DB를 직접 조회할 때 spring.h2.console.enabled: true ✅ ✅ 이해함 🔺 애매함 ❌ 아직 📖 오늘의 주요 학습 내용 1. 게이트웨이 라우트: 경로의 첫 마디로 세 서비스를 구분한다 🍞 개념 비유 우체국에서 우편번호 앞자리만 보고 지역별 함에 나눠 넣는 것과 비슷하다. 분류하는 사람은 받는 사람의 집 위치를 몰라도 된다. 우편번호 앞자리가 경로의 첫 마디( /user-service ), 지역별 함이 서비스, 분류하는 사람이 게이트웨이에 해당한다. 지난 실습의 first-service , second-service 에 더해 user-service , catalog-service , order-service 세 모듈이 추가되었다. 게이트웨이에는 서비스마다 라우트를 하나씩 등록하며, 경로가 /user-service/ 로 시작하면 USER-SERVICE 로 넘기는 방식이다. routes: - id: user-service uri: lb://USER-SERVICE predicates: - Path=/user-service/** - id: catalog-service uri: lb://CATALOG-SERVICE predicates: - Path=/catalog-service/** - id: order-service uri: lb://ORDER-SERVICE predicates: - Path=/order-service/** 게이트웨이는 경로를 바꾸지 않고 그대로 전달하므로, 각 서비스의 컨트롤러도 같은 경로로 시작해야 한다. @RestController //@RequestMapping("/") @RequestMapping("/user-service") @Slf4j public class UserController { 다섯 서비스를 모두 띄운 뒤 Eureka에 등록된 목록이다. 서비스 이름, 포트, 상태만 추렸다. CATALOG-SERVICE 60571 UP ORDER-SERVICE 58567 UP APIGATEWAY-SERVICE 8000 UP USER-SERVICE 60000 UP 8000번 포트 하나로 세 서비스의 health-check 를 호출한 결과다. == http://localhost:8000/user-service/health-check It's Working in User Service, port(local.server.port)=60000, port(server.port)=60000 [HTTP 200] == http://localhost:8000/catalog-service/health-check It's Working in Catalog Service on LOCAL PORT 60571 (SERVER PORT 0) [HTTP 200] == http://localhost:8000/order-service/health-check It's Working in Order Service on LOCAL PORT 58567 (SERVER PORT 0) [HTTP 200] ⚠️ server.port 는 설정 파일에 적은 값이고 local.server.port 는 실제로 열린 포트다. catalog-service 와 order-service 는 포트를 0으로 설정했으므로 SERVER PORT 0 으로 출력되며, 실제 포트는 기동할 때마다 달라진다. user-service 만 실습 편의상 60000으로 고정되어 있다. 2. 사용자 등록: 요청 객체를 DTO와 엔티티로 옮겨 담고 비밀번호를 암호화해 저장한다 🍞 개념 비유 병원에서 같은 환자에 대해 접수 서류, 진료 기록, 처방전의 양식이 각각 다른 것과 비슷하다. 접수 서류에는 환자가 적은 내용만 있고, 진료 기록에는 병원이 부여한 환자 번호가 추가되며, 처방전에는 약을 받는 데 필요한 내용만 적힌다. 접수 서류가 RequestUser , 진료 기록이 UserEntity , 처방전이 ResponseUser 에 해당한다. 회원 한 명의 정보는 네 개의 객체를 거친다. RequestUser 는 요청 본문을 받는 객체, UserDto 는 컨트롤러와 서비스 사이에서 전달하는 객체, UserEntity 는 users 테이블과 매핑되는 객체, ResponseUser 는 응답 본문으로 나가는 객체다. 객체 사이의 값 복사는 ModelMapper 가 담당한다. 이름이 같은 필드끼리 값을 옮겨 주며, MatchingStrategies.STRICT 로 설정하면 이름이 정확히 일치하는 필드만 옮긴다. @PostMapping("/users") public ResponseEntity<ResponseUser> createUser(@RequestBody RequestUser user) { ModelMapper mapper = new ModelMapper(); mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); // 요청 객체 -> DTO UserDto userDto = mapper.map(user, UserDto.class); userService.createUser(userDto); // DTO -> 응답 객체 (pwd, encryptedPwd 필드가 없으므로 비밀번호는 응답에서 빠진다) ResponseUser responseUser = mapper.map(userDto, ResponseUser.class); return ResponseEntity.status(HttpStatus.CREATED).body(responseUser); } 서비스에서는 외부에 노출할 식별자인 userId 를 UUID로 발급하고, 비밀번호를 BCryptPasswordEncoder 로 암호화한 뒤 저장한다. @Override public UserDto createUser(UserDto userDto) { // DB의 id(1, 2, 3...) 대신 외부에 보여 줄 식별자 userDto.setUserId(UUID.randomUUID().toString()); ModelMapper mapper = new ModelMapper(); mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); UserEntity userEntity = mapper.map(userDto, UserEntity.class); // userEntity.setEncryptedPwd("encrypted_password"); // 원문 pwd는 엔티티에 필드가 없어 옮겨지지 않고, 암호화한 값만 담는다 userEntity.setEncryptedPwd(passwordEncoder.encode(userDto.getPwd())); userRepository.save(userEntity); UserDto returnUserDto = mapper.map(userEntity, UserDto.class); return returnUserDto; } BCryptPasswordEncoder 는 UserServiceApplication 에서 빈으로 등록하고, UserServiceImpl 이 생성자로 주입받는다. @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } users 테이블과 매핑되는 엔티티다. id 는 DB가 자동으로 증가시키는 값이고, userId 는 서비스에서 발급한 UUID다. @Data @Entity @Table(name = "users") public class UserEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 50, unique = true) private String email; @Column(nullable = false, length = 50) private String name; @Column(nullable = false, unique = true) private String userId; @Column(nullable = false, unique = true) private String encryptedPwd; } 등록 요청의 본문은 RequestUser 의 필드 세 개로 구성한다. email 과 name 은 실습에서 등록한 값이다. POST /user-service/users Content-Type: application/json { "email": "test@naver.com", "name": "이상혁", "pwd": "(직접 정한 비밀번호)" } 등록한 뒤 전체 사용자를 조회한 결과다. userId 에 UUID가 발급되어 있고, 비밀번호와 관련된 항목은 응답에 없다. orders 는 값이 null 이므로 @JsonInclude(JsonInclude.Include.NON_NULL) 에 의해 생략되었다. == http://localhost:8000/user-service/users [{"email":"test@naver.com","name":"이상혁","userId":"cb943bbd-46da-4ddc-974b-bd4533acecf5"}] [HTTP 200] ⚠️ 컨트롤러는 userService.createUser(userDto) 의 반환값을 받지 않고, 처음에 만든 userDto 로 응답 객체를 만든다. 서비스가 같은 객체에 setUserId() 를 호출했기 때문에 응답에 userId 가 포함된다. ⚠️ RequestUser 에 @NotNull , @Size(min = 8) , @Email 이 붙어 있지만, 컨트롤러의 인자에 @Valid 가 없으므로 현재 코드에서는 검증이 실행되지 않는다. 3. SecurityFilterChain: H2 콘솔만 열어 두고 나머지 요청에는 Basic 인증을 요구한다 🍞 개념 비유 아파트 단지의 놀이터와 동 출입문의 차이와 비슷하다. 놀이터는 누구나 들어갈 수 있지만, 동 출입문은 비밀번호를 눌러야 열린다. 놀이터가 permitAll() 로 열어 둔 경로, 동 출입문이 그 밖의 모든 요청, 출입문 비밀번호가 Basic 인증 정보에 해당한다. user-service 에는 spring-boot-starter-security 가 들어 있으며, 어떤 요청을 허용하고 어떤 요청에 인증을 요구할지를 SecurityFilterChain 빈으로 정한다. @Bean protected SecurityFilterChain configure(HttpSecurity http) throws Exception { http.csrf( (csrf) -> csrf.disable()) .authorizeHttpRequests(auth -> auth // .requestMatchers("/**").permitAll() // 모든 경로 허용 .requestMatchers("/h2-console/**").permitAll() // 특정 경로 허용 .anyRequest().authenticated() // 그 외는 인증 필요 ) .httpBasic(Customizer.withDefaults()) // ← Basic 인증 추가 .headers((headers) -> headers .frameOptions((frameOptions) -> frameOptions.sameOrigin())); return http.build(); } 각 줄의 역할은 다음과 같다. csrf.disable() : 브라우저 폼이 아닌 API 호출이므로 CSRF 토큰 검사를 끈다. 켜 두면 POST 요청이 거절된다. requestMatchers("/h2-console/**").permitAll() : H2 콘솔은 인증 없이 접속할 수 있다. anyRequest().authenticated() : 그 밖의 모든 요청은 인증이 필요하다. frameOptions.sameOrigin() : H2 콘솔 화면은 프레임으로 구성되어 있으므로, 같은 출처의 프레임은 허용한다. Basic 인증에 사용하는 계정은 application.yml 의 spring.security.user 에 적는다. BCryptPasswordEncoder 가 빈으로 등록되어 있으므로 비밀번호는 원문이 아니라 BCrypt로 암호화한 값을 적어야 한다. security: user: name: user # password: password password: $2a$10$g70r... # BCrypt로 암호화한 값 (뒷부분 생략) 암호화한 값은 Utils 의 main() 을 실행해서 얻는다. public class Utils { public static void main(String[] args) { System.out.println(new BCryptPasswordEncoder().encode("password")); } } 인증 정보 없이 호출하면 서비스의 컨트롤러까지 가지 않고 401이 응답된다. == http://localhost:8000/user-service/users HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="Realm" X-Frame-Options: SAMEORIGIN Content-Length: 0 Basic 인증 정보를 함께 보내면 200이 응답된다. == http://localhost:8000/user-service/welcome Welcome to the Simple E-commerce. [HTTP 200] ⚠️ 인증은 게이트웨이가 아니라 user-service 가 처리한다. 게이트웨이는 Authorization 헤더를 그대로 전달하며, catalog-service 와 order-service 에는 Security 의존성이 없으므로 인증 없이 호출된다. ⚠️ 존재하지 않는 userId 로 상세 조회를 하면 404가 아니라 401이 응답된다. getUserByUserId() 가 던지는 UsernameNotFoundException 이 인증 예외의 한 종류이므로, Spring Security가 인증 실패로 처리하기 때문이다. == http://localhost:8000/user-service/users/no-such-id {"timestamp":"2026-10-07T08:16:42.103+00:00","status":401,"error":"Unauthorized","trace":"org.springframework.security.core.userdetails.UsernameNotFoundException: User not found\r\n\tat com.example.userservice.service.UserServiceImpl.getUserByUserId(UserServiceImpl.java:56) ...","message":"Unauthorized","path":"/user-service/users/no-such-id"} [HTTP 401] 4. data.sql: 서버가 기동할 때 상품 초기 데이터를 넣는다 🍞 개념 비유 편의점이 문을 열기 전에 진열대에 상품을 채워 두는 것과 비슷하다. 진열대가 먼저 설치되어 있어야 상품을 올릴 수 있다. 진열대 설치가 Hibernate의 테이블 생성, 상품을 채우는 일이 data.sql 실행, 설치가 끝난 뒤에 채우도록 순서를 정하는 것이 defer-datasource-initialization 에 해당한다. catalog-service 에는 상품을 등록하는 API가 없다. 대신 src/main/resources/data.sql 에 적은 INSERT 문이 서버가 기동할 때 실행된다. insert into catalog(product_id, product_name, stock, unit_price) values('CATALOG-001', 'Berlin', 100, 1500); insert into catalog(product_id, product_name, stock, unit_price) values('CATALOG-002', 'Tokyo', 110, 1000); insert into catalog(product_id, product_name, stock, unit_price) values('CATALOG-003', 'Stockholm', 120, 2000); data.sql 은 기본적으로 Hibernate가 테이블을 만들기 전에 실행된다. 테이블이 없는 상태에서 INSERT 가 먼저 실행되지 않도록 defer-datasource-initialization: true 로 실행 시점을 테이블 생성 뒤로 미룬다. jpa: hibernate: ddl-auto: create-drop show-sql: true generate-ddl: true database: h2 defer-datasource-initialization: true # data.sql / schema.sql 실행을 Hibernate가 테이블을 만든 뒤로 미루는 설정 data.sql 에는 created_at 을 적지 않았다. 엔티티에서 insertable = false 로 INSERT 문에서 이 컬럼을 빼고, @ColumnDefault 로 DB의 기본값이 들어가도록 했기 때문이다. @Data @Entity @Table(name = "catalog") public class CatalogEntity { // implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 120, unique = true) private String productId; @Column(nullable = false) private String productName; @Column(nullable = false) private Integer stock; @Column(nullable = false) private Integer unitP
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
LG CNS AM 6기 39일차 TIL — 사용자 · 상품 · 주문 서비스 분리와 서비스별 DB. MSA에서는 사용자, 상품, 주문 서비스가 게이트웨이의 경로로 구분되어 호출되며, 각 서비스는 자신의 데이터베이스만 조회한다. 따라서 서비스끼리 연동하기 전에는 주문이 저장되어 있어도 사용자 상세 조회의 주문 내역이 빈 리스트로 응답된다. 📑 목차 🔑 상황별 문법 찾기 📖 오늘의 주요 학습 내용 🚨 막혔던 지점 🎯 다음에 할 일 🌱 오늘 성장 기록 🔑 상황별 문법 찾기 이런 상황 쓰는 문법 이해도 게이트웨이에서 경로로 서비스를 나눌 때 uri: lb://USER-SERVICE + Path=/user-service/** ✅ 요청 객체의 값을 다른 객체로 옮겨 담을 때 mapper.map(user,…
Открыть источник