MixProject에서 한 동물 합성 결과를 UserProject의 유저 인벤토리에 저장해야한다.
Producer(Mix Project)
설정 Config
방법 1. resource - application.yml
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
aplication.yml에서 설정한다면, producer와 Consumer를 둘 다 넣어두는게 나중에 잊지 않고 사용 할 수 있어서 좋을 것 같다. 오류가 나면 어디인지 찾는데에 시간이 걸릴 수 있으니..
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
consumer:
auto-offset-reset: latest
group-id: community
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.ByteArrayDeserializer
properties:
spring.json.trusted.packages: "*"
방법 2. ProducerConfig 파일을 따로 만들어 준다.
코드가 더 짦아지기 때문에 aplication.yml에 하려고 했으나, JsonConverter가 없어서 오류가 났다.
그래서 ProducerConfig 파을을 따로 만들어 줬다.
@Configuration
public class ConsumerConfig {
// 받을때 JSON 받겠다.
@Bean
public RecordMessageConverter converter() {
return new JsonMessageConverter();
}
// 만약 data가 넘어오는 도중에 에러가 낫다면 1초에 2번씩 더 요청할 것이다.
@Bean
public CommonErrorHandler errorHandler(KafkaOperations<Object, Object> kafkaOperations) {
return new DefaultErrorHandler(
new DeadLetterPublishingRecoverer(kafkaOperations),
new FixedBackOff(1000L, 2)
);
}
}
TopicConfig
mixResult라는 주제를 만들어 줬다.
@Component
public class TopicConfig {
// consumer 설정
// 1. producer 쪽에서 만든 topic을 똑같이 정의해줌
public final static String mixRequest = "mixRequest";
// 2. 정의한걸로 만들어 줌
// 3. topic 을 만들었으면 여긴 받는 사람이니까 그 topic을 수신하는 처리를 해야함.
// AnimalMixConsumer 로 가보자
@Bean
public NewTopic mixRequestTopic() {
return new NewTopic(mixRequest, 1, (short) 1);
}
}
MixProgucer
@Service
@RequiredArgsConstructor
public class MixProducer {
// 4. 이곳에서 보내는 카프카 템플릿을 정의한다.
private final KafkaTemplate<String, MixRequest> mixKafkaTemplate;
// 5. 내가 보내고 싶은 topic 으로 보낸다.
public void sendMixResult(MixRequest mixRequest) {
// 6. 카프카로 해당하는 주제로 data를 보낸다
// mixKafkaTemplate.send(topic이름, 실제 전송하고자 하는 데이터)
CompletableFuture<SendResult<String, MixRequest>> send = mixKafkaTemplate.send(TopicConfig.mixResult, mixRequest);
send.thenAccept(x -> {
System.out.println("전송다됨");
});
}
// 6. 그럼 이제 카프카로 보내는 기능은 만들어졌으니 그걸 어디에 쓸까 ? 이건 내 로직에 따라 다름.
// 내가 카프카로 보내는 걸 써야할 로직은 AnimalService 여기에 있는 것 같으니 여기로 가보자.
}
MixService
만들어둔 Producer를 쓰자
아래처럼 MixResult를 받아서 넣기
합성 성공하면 나온 동물을 MixResult값에 넣어준다.
그럼 그 값을 Producer메소드인 sendMixResult에 넣어준다.
Mix startedMixAnimal = startMixAnimal(mixRequestDto.getGrade(), token);
mix = mixRepository.save(startedMixAnimal);
// userAnimalList를 추가 했기 때문에 Mix -> MixRequest로 바꿔줘야함..
MixRequest mixRequest = MixRequest.builder()
.id(mix.getId())
.userUUID(mix.getUserUUID())
.userId(mix.getUserId())
.nickName(mix.getNickName())
.entityType(mix.getEntityType())
.animalId(mix.getAnimalId())
.name(mix.getName())
.grade(mix.getGrade())
.userAnimalList(selectedUserAnimalList)
.build();
// 동물 합성 결과를 user로 보내기(producer 여기서 사용!!)
mixProducer.sendMixResult(mixRequest);
전체코드 (아직 최종 DB와 화면을 연결해서 진행한 것은 아니라서 수정이 될 예정이다.)
package com.example.animalwarmix.service;
import com.example.animalwarmix.common.RestError;
import com.example.animalwarmix.common.RestResult;
import com.example.animalwarmix.config.JwtService;
import com.example.animalwarmix.config.TokenInfo;
import com.example.animalwarmix.domain.request.MixRequest;
import com.example.animalwarmix.domain.entity.*;
import com.example.animalwarmix.domain.dto.MixRequestDto;
import com.example.animalwarmix.kafka.MixConsumer;
import com.example.animalwarmix.kafka.MixProducer;
import com.example.animalwarmix.repository.AnimalRepository;
import com.example.animalwarmix.repository.BuildingRepository;
import com.example.animalwarmix.repository.MixRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Random;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class MixService {
private final AnimalRepository animalRepository;
private final BuildingRepository buildingRepository;
private final MixRepository mixRepository;
private final JwtService jwtService;
private final MixProducer mixProducer;
private final MixConsumer mixConsumer;
// 왜 구별 해야하는가? animalRepository, buildingRepository 중 어떤걸 쓸지 선택해야해서..
public ResponseEntity<RestResult<Object>> startMix(MixRequestDto mixRequestDto, List<Long> selectedUserAnimalList) {
// Token 받기
TokenInfo token = jwtService.parseAccessToken(mixRequestDto.getAccessToken());
Mix mix = new Mix();
// #1 동물 합성인지, 건물 합성인지
// 동물 합성
if(EntityType.ANIMAL.equals(mixRequestDto.getEntityType())) {
try {
Mix startedMixAnimal = startMixAnimal(mixRequestDto.getGrade(), token);
mix = mixRepository.save(startedMixAnimal);
// userAnimalList를 추가 했기 때문에 Mix -> MixRequest로 바꿔줘야함..
MixRequest mixRequest = MixRequest.builder()
.id(mix.getId())
.userUUID(mix.getUserUUID())
.userId(mix.getUserId())
.nickName(mix.getNickName())
.entityType(mix.getEntityType())
.animalId(mix.getAnimalId())
.name(mix.getName())
.grade(mix.getGrade())
.userAnimalList(selectedUserAnimalList)
.build();
// 동물 합성 결과를 user로 보내기
mixProducer.sendMixResult(mixRequest);
} catch (Exception e) { // 합성 실패
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new RestResult<>("error", new RestError("MIX FAIL","합성에 실패했습니다.")));
}
} else { // 건물 합성
try {
Mix startedMixBuilding = startMixBuilding(mixRequestDto.getGrade(), token);
mix = mixRepository.save(startedMixBuilding);
MixRequest mixRequest = MixRequest.builder()
.id(mix.getId())
.userUUID(mix.getUserUUID())
.userId(mix.getUserId())
.nickName(mix.getNickName())
.entityType(mix.getEntityType())
.animalId(mix.getAnimalId())
.name(mix.getName())
.grade(mix.getGrade())
.userAnimalList(selectedUserAnimalList)
.build();
// 건물 합성 결과를 user로 보내기
mixProducer.sendMixResult(mixRequest);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new RestResult<>("error", new RestError("MIX FAIL","합성에 실패했습니다.")));
}
}
return ResponseEntity.ok(new RestResult<>("success", mix));
}
// ### 동물 합성 ###
// #2 합성 시작
public Mix startMixAnimal(Grade currentGrade, TokenInfo token) {
// 합성 성공 확률 (15%)
int successRate = 100;
// 0부터 99까지의 난수 생성
int randomNumber = new Random().nextInt(100);
// 합성실패
if (randomNumber > successRate) {
// System.out.println("합성 실패"); // TODO 합성 실패/성공 후 유저 서비스에서 삭제 구현
// throw new IllegalAccessError("합성 실패"); // TODO 오류를 던졌을 때, startMix()에서 try-catch에서 오류를 잡지 못함..된다면, 탈렌드에서 "합성에 실패했습니다." 떠야함..
return null;
}
// 합성 성공
Grade nextGrade = determineNextGrade(currentGrade);
// #3 다음 등급 동물 랜덤 1개 주기..
return mixAnimalSuccess(nextGrade, token);
}
// #3 다음 등급 동물 랜덤 1개 주기..
public Mix mixAnimalSuccess(Grade nextGrade, TokenInfo token) {
Animal byAnimalMix = animalRepository.findByAnimalMix(nextGrade);
Mix mix = Mix.builder()
.userUUID(UUID.fromString(token.getUserUUID()))
.userId(token.getId())
.nickName(token.getNickName())
.entityType(EntityType.ANIMAL)
.name(byAnimalMix.getName())
.grade(byAnimalMix.getGrade())
.animalId(byAnimalMix.getAnimalId())
.build();
return mix;
}
// 한 단계 상위 등급으로 바꿔주기
public Grade determineNextGrade(Grade currentGrade) {
switch (currentGrade) {
case NORMAL -> {return Grade.RARE;}
case RARE -> {return Grade.SUPERRARE;}
case SUPERRARE -> {return Grade.UNIQUE;}
case UNIQUE -> {return Grade.LEGEND;}
default -> {return null;}
}
}
// ### 건물 합성 ###
// #2 합성 시작
public Mix startMixBuilding(Grade currentGrade, TokenInfo token) {
// 합성 성공 확률 (15%)
int successRate = 100;
// 0부터 99까지의 난수 생성
int randomNumber = new Random().nextInt(100);
// 합성 실패
if (randomNumber > successRate) {
// System.out.println("합성 실패"); // TODO 합성 실패/성공 후 유저 서비스에서 삭제 구현
// throw new IllegalAccessError("합성 실패"); // TODO 오류를 던졌을 때, startMix()에서 try-catch에서 오류를 잡지 못함..된다면, 탈렌드에서 "합성에 실패했습니다." 떠야함..
return null;
}
// 합성 성공
Grade nextGrade = determineNextGrade(currentGrade);
// #3 다음 등급 동물 랜덤 1개 주기..
return mixBuildingSuccess(nextGrade, token);
}
// #3 다음 등급 동물 랜덤 1개 주기..
public Mix mixBuildingSuccess(Grade nextGrade, TokenInfo token) {
Building byBuildingMix = buildingRepository.findByBuildingMix(nextGrade);
System.out.println(byBuildingMix);
Mix mix = Mix.builder()
.userUUID(UUID.fromString(token.getUserUUID()))
.userId(token.getId())
.nickName(token.getNickName())
.entityType(EntityType.BUILDING)
.name(byBuildingMix.getName())
.grade(byBuildingMix.getGrade())
.build();
return mix;
}
}
MixRequest
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MixRequest {
private Long id;
private UUID userUUID;
private String userId;
private String nickName;
private EntityType entityType;
private Long animalId;
private String name;
private Grade grade;
private List<Long> userAnimalList;
public Mix toEntity() {
return Mix.builder()
.id(id)
.userUUID(userUUID)
.userId(userId)
.nickName(nickName)
.entityType(entityType)
.animalId(animalId)
.name(name)
.grade(grade)
.build();
}
}
Consumer(User Project)
Config 설정
방법 1. resource - application.yml
kafka:
bootstrap-servers: localhost:9092
consumer:
auto-offset-reset: latest
group-id: community
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.ByteArrayDeserializer
properties:
spring.json.trusted.packages: "*"
방법 2. ConsumerConfig 파일을 따로 만들어 준다.
// consumer 쪽의 환경설정
@Configuration
public class ConsumerConfig {
// 받을때 JSON 받겠다.
@Bean
public RecordMessageConverter converter() {
return new JsonMessageConverter();
}
// 만약 data가 넘어오는 도중에 에러가 낫다면 1초에 2번씩 더 요청할 것임.
@Bean
public CommonErrorHandler errorHandler(KafkaOperations<Object, Object> kafkaOperations) {
return new DefaultErrorHandler(
new DeadLetterPublishingRecoverer(kafkaOperations),
new FixedBackOff(1000L, 2)
);
}
}
TopicConfig
데이터를 보내는 사람(Producer) 기준으로 Topic을 설정해준다.
@Component
public class TopicConfig {
// 보내는 사람(Producer) 기준
// 1. topic 의 이름을 정의한다.
public final static String mixResult = "mixResult";
// 2. 정의한 topic 의 이름으로 topic 을 생성한다.
@Bean
public NewTopic mixResultTopic() {
return new NewTopic(mixResult, 1, (short) 1);
}
// 3. topic 을 생성 했으니.. 이제 그 topic으로 data를 보내보자.. AnimalMixProducer.java 로 가보자..
}
MixConsumer
🟢🟡🟠 주의
Producer에서 보낸 데이터인 MixRequest 와 동일한 구조를 가지고 있어야 한다.
MixProject의 MixRequest 를 UserProject의 MixRequest로 받아줬다.
@Service
@RequiredArgsConstructor
public class MixConsumer {
private final UserAnimalRepository userAnimalRepository;
private final MixRepository mixRepository;
private final MixService mixService;
// 4. 해당 주제를 구독하는 처리를 먼저 해준다.
@KafkaListener(topics = TopicConfig.mixRequest) // TopicConfig에 정의해 준 mixRequest 구독한다.
public void mixResultListen(MixRequest mixRequest) { // 객체 MixRequest를 받을 것이다.
mixService.saveInventoryAndDeleteMixed(mixRequest);
// 받은 mixRequest는 mixservice의 saveInventoryAndDeleteMixed(mixRequest)메소드에서 사용
}
}
MixRequest
UserProject의 MixRequest와 동일하게 해줘야한다!!!꼭!!
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MixRequest {
private Long id;
private UUID userUUID;
private String userId;
private String nickName;
private EntityType entityType;
private Long animalId;
private String name;
private Grade grade;
private List<Long> userAnimalList;
public Mix toEntity() {
return Mix.builder()
.id(id)
.userUUID(userUUID)
.userId(userId)
.nickName(nickName)
.entityType(entityType)
.animalId(animalId)
.name(name)
.grade(grade)
.build();
}
}
MixService
userAnimalRepository(=인벤토리)에 저장
@Transactional //saveInventory와 deleteMixed 하나라도 실패하면 둘 다 하면 안됨..
public void saveInventoryAndDeleteMixed(MixRequest mixRequest) {
try {
// saveInventory 실행
saveInventory(mixRequest);
// deleteMixed 실행
userAnimalRepository.deleteAllByAnimal_AnimalIdIn(mixRequest.getUserAnimalList());
// deleteMixed(mixRequest.getUserAnimalList());
} catch (Exception e) {
System.out.println("합성에 실패했습니다. 관리자에게 문의해주세요.");
}
}
MixService 전체코드
package com.example.aniamlwaruser.service;
import com.example.aniamlwaruser.domain.entity.Animal;
import com.example.aniamlwaruser.domain.entity.Mix;
import com.example.aniamlwaruser.domain.entity.UserAnimal;
import com.example.aniamlwaruser.domain.request.MixRequest;
import com.example.aniamlwaruser.repository.AnimalRepository;
import com.example.aniamlwaruser.repository.UserAnimalRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class MixService {
private final AnimalRepository animalRepository;
private final UserAnimalRepository userAnimalRepository;
// TODO 합성 실패/성공했을 때 메시지 띄우기(오류처리)..
@Transactional //saveInventory와 deleteMixed 하나라도 실패하면 둘 다 하면 안됨..
public void saveInventoryAndDeleteMixed(MixRequest mixRequest) {
try {
// saveInventory 실행
saveInventory(mixRequest);
// deleteMixed 실행
userAnimalRepository.deleteAllByAnimal_AnimalIdIn(mixRequest.getUserAnimalList());
// deleteMixed(mixRequest.getUserAnimalList());
} catch (Exception e) {
System.out.println("합성에 실패했습니다. 관리자에게 문의해주세요.");
}
}
// #4 유저 인벤토리에 넣기
// 1. 믹스된걸 받은 것을 참고해서 도감에서 해당하는 걸 찾아
// 2. 찾은걸 인벤토리에(UserAnimal) 넣어
public void saveInventory(MixRequest mixRequest) {
// 합성 완료한 동물 도감에서 찾기
Optional<Animal> animalById = animalRepository.findById(mixRequest.getAnimalId());
// 인벤토리에 있는 동물 확인
Optional<UserAnimal> currentAnimalById = userAnimalRepository.findByAnimal_AnimalId(mixRequest.getAnimalId());
if(currentAnimalById.isPresent()){
UserAnimal userAnimal = UserAnimal.builder()
.id(currentAnimalById.get().getId())
.user(currentAnimalById.get().getUser())
.animal(animalById.get()) // 합성 완료한 동물
.ownedQuantity(currentAnimalById.get().getOwnedQuantity() +1) // 보유 개수
.batchedQuantity(currentAnimalById.get().getBatchedQuantity()) // 배치 개수
.upgrade(0) // 새로 받은 캐릭의 강화단계는 디폴트 0
.build();
userAnimalRepository.save(userAnimal);
} else {
throw new IllegalArgumentException("해당 동물을 찾을 수 없습니다.");
}
}
// #5 유저 인벤토리에서 믹스에 사용한 4개를 삭제
// public void deleteMixed(List<Long> selectedUserAnimalList) {
// userAnimalRepository.deleteAllById(selectedUserAnimalList);
// }
}
'Project > Collabo Project' 카테고리의 다른 글
[Villion] Docker를 실행 할 때 데이터 넣기 (0) | 2024.05.30 |
---|---|
[Villion] 마이크로 서비스 간의 통신을 위해 Feign Client vs. Kafka 중 어떤 것을 사용할까? (0) | 2024.05.02 |
[동물전쟁] Socket으로 전체 공개 채팅 만들기(MySQL저장) (0) | 2023.10.22 |
[동물전쟁] Socket에 Token받아오기 (0) | 2023.10.22 |
직무별 채용 사이트 추천 서비스 (1) | 2023.09.06 |