구현 해야 하는 기능.
유저
- 헤더에서 토큰 값 가져와 유저 구분하기 (uuid)
- 유저 생성 ( RabbitMq 이벤트 처리 )
- 유저 수정 (API)
- 유저 프로필 조회(API)
헤더에서 토큰 값 가져와 유저 구분하기
MSA 패턴에서 모든 요청은 GateWay 서버로 들어온다.
때문에 GateWay 에서 유저 인증, 인가처리에 대해서 모두 마친 후, 인증된 데이터를 토큰으로 만들어 헤더에 담는다.
그 헤더를 다시 해당하는 url에 맞추어 각 서버로 API 요청을 보낸다.
때문에 우리는 헤더의 유효성을 체크하지 않고, 그저 데이터를 가져다 사용하기만 하면 된다.
public ResponseEntity<?> getProfile(@RequestHeader("Authorization") String data) throws NoResourceException {
//@RequestHeader("Authorization") 을 통해 헤더 값을 가져올 수 있다.
String[] chunks = data.split("\\.");
Base64.Decoder decoder = Base64.getDecoder();
String payload = new String(decoder.decode(chunks[1])); //복호화 과정
JSONObject jsonObject = new JSONObject(payload); //문자열을 직렬화시킴
String sample = jsonObject.getString("uid"); //uid 키 값을 가져옴
UUID uuid = UUID.fromString(sample); //uuid 객체로 변환
User findUser = userService.findUser(uuid); //유저를 찾음
int httpStatus = HttpStatusChangeInt.ChangeStatusCode("OK");
String path = "/users/";
ResponseDetails responseDetails = new ResponseDetails(new Date(), findUser, httpStatus, path);
return new ResponseEntity<>(responseDetails, HttpStatus.CREATED);
}
1. 헤더에 있는 Authorization 값을 가져와 Base64 기법으로 복호화를 진행 한다.
2. 복호화된 값은 byte 코드이기 때문에 문자열로 형변환 시켜준다.
3. { 키 : 값, 키 : 값 ...} 으로 되어 있는 문자열을 직렬화 시켜준다.
4. 키 값이 uid 인 값은 Object 값이기 때문에 getString 으로 가져온다.
5. uuid 값으로 유저를 검색, 식별 하기 때문에 다시 uuid 형태로 바꿔준다.
6. 로직을 실행한다.
유저 생성 ( RabbitMq 이벤트 처리 )
게이트웨이에서 회원가입 정보들을 받으면, 유저 도메인 쪽으로 입력 받은 정보들을 저장해주어야 한다.
물론 API 를 통해, 동기로 처리할 수 있다. 하지만 유저 도메인에서 회원가입 로직을 처리할 때까지, 게이트웨이는 대기 상태가 된다.
모든 요청이 게이트웨이로 들어올텐데, 대기 상태가 되면 MSA 아키텍처로 구성한 이점이 사라지게 된다.
때문에 메시지큐를 통한 비동기 이벤트 처리로 서버간 통신하게끔 구현하였다.
그 중에서도 RabbitMQ 이벤트 처리로 구현하였다.
이번 포스팅은 프로젝트에 개발 상황에 집중하였으므로, RabbitMQ 에 대한 자세한 설명은 생략하겠다.
기존 아키텍처라면 게이트웨이 서버가 메시지 프로듀서가 되며, 미리 약속한 exchange 에 메시지를 보낼 것이다.
하지만 아직 온전한 게이트웨어 서버가 배포되지 않았으므로, 로컬 환경에 새로운 프로듀서 프로젝트를 만들었다.
RabbitMqController
@RestController
@RequestMapping(value = "/api/v1/")
public class ProducerController {
// RabbitMQ에 메시지를 보내기 위한 객체
private RabbitMqSender rabbitMqSender;
@Autowired
public ProducerController(RabbitMqSender rabbitMqSender) {
this.rabbitMqSender = rabbitMqSender;
}
// yml 파일의 message 를 가져옴
@Value("${app.message}")
private String message;
// User 개체를 사용하여 RabbitMQ로 보냄
@PostMapping(value = "user")
public String publishUserDetails(@RequestBody CustomMessage customMessage) {
rabbitMqSender.send(customMessage);
return message;
}
}
메시지 프로듀서 컨트롤러의 동작은 다음과 같다.
1. "/api/v1/user/" Post 요청이 들어온다.
2. http 요청의 body 값을 가져와 rabbitMqSender 객체의 send 의 파라미터 값으로 넘겨준다.
3. 해당 메소드의 실행이 끝나면, application.yml 의 app.message 값을 반환한다.
RabbitMqSender
@Service
public class RabbitMqSender {
// RabbitTemplate 클래스를 사용하면 RabbitMQ로 메시지를 보내고 받을 수 있다.
private RabbitTemplate rabbitTemplate;
@Autowired
public RabbitMqSender(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
@Value("${spring.rabbitmq.exchange}")
private String exchange;
@Value("${spring.rabbitmq.routingkey}")
private String routingkey;
public void send(CustomMessage customMessage) {
rabbitTemplate.convertAndSend(exchange, routingkey, customMessage);
}
}
메시지 프로듀서 컨트롤러의 동작은 다음과 같다.
1. 미리 설정해둔 RabbitTemplate 을 DI 한다.
2. 역시나 application.yml 에 미리 지정해둔 교환기 값에 바인딩 한 key 값을 포함해, 해당 메시지와 함께 전송한다.
3. 메시지를 받은 교환기는 로직에 맞게 바인딩 한 대기 큐에 해당 메시지를 넣는다.
유저 도메인에 회원가입 처리를 할 예정이라, 유저 도메인을 먼저 만들어보겠다.
User
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) //외부 생성 금지
@Table(name = "user")
public class User extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; //고유 식별자 값
@Column(unique = true, columnDefinition = "BINARY(16)", name = "user_id")
private UUID userId; //유저 식별 id 값
@Email
@Column(nullable = false, unique = true, length = 30)
private String email; //유저 이메일
@Column(nullable = false, unique = true, length = 8)
private String name; //유저 닉네임 최대 8글자
// profile :
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "resume_id")
private Resume resume; //유저 이력서 -> 한 유저당 하나의 이력서만 가능.
/*생성 메서드 !!!! 반드시 static*/
public static User signUp(UUID uuid, String email, String name) {
User user = new User();
user.userId = uuid;
user.email = email;
user.name = name;
return user;
}
}
유저는 다음과 같은 필드 값을 가진다.
- id (pk)
- uuid (식별자로 사용)
- email (회원가입 id)
- name (닉네임)
- resume (이력서) => 한 계정당 하나의 이력서만 가능하다. 우선은 지연로딩으로 필요할 때 쿼리가 나가게끔 구현하였다.
- profile (사진) => s3에 올릴 이미지의 엔드포인트 url 이다. 우선은 생략하였다.
외부에서 실수로 생성하지 못 하게, 기본 생성자 접근을 protected 레벨로 막아두었다.
그리고 생성자 메소드를 정적으로 만들어, 생성자 메소드를 통해서만 생성할 수 있게 예방하였다.
나머지 필드값은 null 값이 가능하며, 후에 수정 메소드에서 값을 추가할 수 있게 구현하였다.
UserRepository
@Repository
@RequiredArgsConstructor
public class UserRepository {
private final EntityManager em;
public UUID save(User user) {
em.persist(user);
return user.getUserId();
}
}
UserRepository 이다. 실제로 데이터베이스와 간접 통신하는 클래스이다.
EntityManger 를 생성자 주입을 통해 의존관계를 설정하였다.
유저 파라미터 값을 영속성 컨텍스트로 관리한 후, 해당 유저의 uuid 값을 반환한다.
이제 우리는 보낸 메시지를 받아 이벤트 처리하는 메시지 컨슈머를 작성해야한다.
그 전에 우선 Queue, Exchange 를 선언하고 이 둘을 바인딩 해야합니다.
그 후 MessageConvertor, RabbiMqTemplate, ConnectionFactory 도 사용자에 맞게 재정의 해주어야 한다.
application.yml
RabbitMqConfig
@Configuration
public class RabbitMQConfig {
@Value("${spring.rabbitmq.queue}")
private String queue;
@Value("${spring.rabbitmq.exchange}")
private String exchange;
@Value("${spring.rabbitmq.routingkey}")
private String routingKey;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Value("${spring.rabbitmq.host}")
private String host;
@Bean
Queue queue() {
return new Queue(queue, true);
}
@Bean
Exchange myExchange() {
return ExchangeBuilder.directExchange(exchange).durable(true).build();
}
@Bean //queue <-> exchange 바인딩
Binding binding() {
return BindingBuilder
.bind(queue())
.to(myExchange())
.with(routingKey)
.noargs();
}
// localhost, username(ID), password를 이용하여 CashingConnectionFactory로 초기화
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(host);
cachingConnectionFactory.setUsername(username);
cachingConnectionFactory.setPassword(password);
return cachingConnectionFactory;
}
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
// RabbitTemplate 을 ConnectionFactory 이용하여 초기화
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(jsonMessageConverter());
return rabbitTemplate;
}
}
프로듀서가 보낸 메시지에 잘 접근할 수 있게 설정해준 부분이다. 공식문서를 보면 자세하게 공부 할 수 있다.
RabbitMqConfig
@Service
@Slf4j
@RequiredArgsConstructor
public class RabbitMqReceiver implements RabbitListenerConfigurer {
private static final Logger logger = LoggerFactory.getLogger(RabbitMqReceiver.class);
private final UserRepository userRepository;
@Override
public void configureRabbitListeners(RabbitListenerEndpointRegistrar rabbitListenerEndpointRegistrar) {
}
// 소비할 큐를 지정
@Transactional
@RabbitListener(queues = "${spring.rabbitmq.queue}") //유저 큐라고 가정
public void receivedMessage(CustomMessage event) {
logger.info("User Details Received is.. " + event);
// message : "회원가입" 검증 로직 필요 ?
User user = User.signUp(event.getUuid(), event.getEmail(), event.getName());
UUID save_uuid = userRepository.save(user);
}
}
회원가입 로직이다.
우선 미리 설정해둔 큐에 접근해, 메시지가 있는지 RabbitListener 로 확인한다.
프로듀서가 메시지를 보내 접근한 큐에 새로운 메시지가 왔다면, receivedMessage 메소드를 실행하게 된다.
커스텀 메시지 형식에는 유저의 email, name, uuid 값이 있기 때문에 get() 함수를 통해 접근해 값을 가져온다.
그 후 미리 만들어둔 유저 생성자 메소드에 값을 대입한 후, 파라미터를 영속성 컨텍스트로 관리하는 repository.save() 함수의 파라미터 값으로 대입한다.
이때 !! @Transactional 어노테이션이 반드시 있어야 한다. 이 어노테이션이 없으면 그저 영속성 컨텍스트로 등록할 뿐, 데이터베이스에 아무런 커밋도 날라가지 않기 때문이다 ! Service 단에 @Transactional 이 있는 이유이다.
에러 사항 !
문제 없다고 생각한 로직이였는데 갑자기 무한루프가 발생하였다.
처음 쿼리는 제대로 잘 날라가는데, 반복적으로 무한쿼리가 나가고 Unique 한 필드 값이 존재하였기 때문에 계속 error 처리가 나왔다.
throws 를 통한 예외처리 코드가 없었기 때문에, 다시 큐에 메세지를 넣고 다시 뽑고 에러 뜨고 다시 넣고 무한 반복이 발생하였다.
대체 어디가 문제일까 ?
정답은 바로 TimeStamp 였다.
createdAt, modifiedAt 을 자동화 하기 위해 모든 클래스에 추상 클래스인 TimeStamp 값을 구현하게끔 설계하였는데.
스프링 실행 메인 클래스에 @EnableJpaAuditing 어노테이션이 빠져, TimeStamp 에 값이 null 로 들어갔었고,
null 값이 들어가면 안됐기 때문에 예외처리가 발생하여, 메시지를 다시 큐에다 넣고, 그 후에는 무한 루프에 빠진 것이다 ..
어노테이션을 제대로 넣어주니 값이 제대로 들어갔다.
다음 포스팅에서는 유저 수정과 조회 API 를 개발하겠다.
'개인 공부 > Devit' 카테고리의 다른 글
[Devit] #3 Spring Stomp 를 사용해 채팅 구현하기 (0) | 2023.02.24 |
---|---|
[Devit] #1 아키텍처 설계 (0) | 2022.06.29 |