diff --git a/build.gradle b/build.gradle index 6d58ddb..4343738 100644 --- a/build.gradle +++ b/build.gradle @@ -55,10 +55,6 @@ dependencies { annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" - // kafka - implementation 'org.springframework.kafka:spring-kafka' - implementation 'org.springframework.kafka:spring-kafka-test' - // redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 1acf422..322747c 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,39 +1,4 @@ services: - kafka-1: - image: confluentinc/cp-kafka:latest - container_name: kafka-1 - ports: - - "29092:29092" - environment: - KAFKA_NODE_ID: 1 - KAFKA_PROCESS_ROLES: 'broker,controller' - KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-1:9093' - KAFKA_LISTENERS: 'PLAINTEXT://kafka-1:9092,CONTROLLER://kafka-1:9093,PLAINTEXT_HOST://0.0.0.0:29092' - KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka-1:9092,PLAINTEXT_HOST://${EC2_PRIVATE_IP}:29092' - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' - KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' - KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT' - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 - CLUSTER_ID: 'ciWo7IWazngRchmPES6q5A==' - KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' - networks: - - app-network - - kafka-ui: - image: provectuslabs/kafka-ui:latest - container_name: kafka-ui - ports: - - "8090:8080" - environment: - KAFKA_CLUSTERS_0_NAME: local - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka-1:9092 - depends_on: - - kafka-1 - networks: - - app-network - redis: image: redis:7-alpine container_name: redis @@ -57,7 +22,6 @@ services: - "80:8080" environment: SPRING_PROFILES_ACTIVE: prod - SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka-1:9092 DB_HOST: ${DB_HOST} DB_PORT: ${DB_PORT} DB_NAME: ${DB_NAME} @@ -66,8 +30,6 @@ services: depends_on: redis: condition: service_healthy - kafka-1: - condition: service_started restart: unless-stopped networks: - app-network @@ -77,4 +39,4 @@ volumes: networks: app-network: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/src/main/java/com/practice/course_registration/domain/member/domain/Member.java b/src/main/java/com/practice/course_registration/domain/member/domain/Member.java index b6d671f..0dd8d61 100644 --- a/src/main/java/com/practice/course_registration/domain/member/domain/Member.java +++ b/src/main/java/com/practice/course_registration/domain/member/domain/Member.java @@ -1,17 +1,11 @@ package com.practice.course_registration.domain.member.domain; -import com.practice.course_registration.domain.subject.domain.LikeSubject; -import com.practice.course_registration.domain.subject.domain.MemberSubject; import com.practice.course_registration.global.common.BaseEntity; import com.practice.course_registration.global.enums.Role; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.ColumnDefault; - -import java.util.ArrayList; -import java.util.List; @Entity @Getter @@ -45,12 +39,6 @@ public class Member extends BaseEntity { @Enumerated(EnumType.STRING) private Role role; - @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) - List memberSubjects = new ArrayList<>(); - - @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) - List likeSubjects = new ArrayList<>(); - public Member(String memberName, String memberNumber, int grade, String loginId, String password) { this.memberName = memberName; this.memberNumber = memberNumber; diff --git a/src/main/java/com/practice/course_registration/domain/member/repository/MemberRepository.java b/src/main/java/com/practice/course_registration/domain/member/repository/MemberRepository.java index 36ea385..fbdaa58 100644 --- a/src/main/java/com/practice/course_registration/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/practice/course_registration/domain/member/repository/MemberRepository.java @@ -1,19 +1,11 @@ package com.practice.course_registration.domain.member.repository; import com.practice.course_registration.domain.member.domain.Member; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; public interface MemberRepository extends JpaRepository { boolean existsByLoginId(String loginId); boolean existsByMemberNumber(String memberNumber); Member findByLoginId(String loginId); - - @Query("select m from Member m " + - "left join fetch m.memberSubjects " + - "where m.id = :id") - Optional findWithSubjectsById(@Param("id") Long id); } diff --git a/src/main/java/com/practice/course_registration/domain/subject/controller/SubjectController.java b/src/main/java/com/practice/course_registration/domain/subject/controller/SubjectController.java index b64c4de..6c99e19 100644 --- a/src/main/java/com/practice/course_registration/domain/subject/controller/SubjectController.java +++ b/src/main/java/com/practice/course_registration/domain/subject/controller/SubjectController.java @@ -6,7 +6,6 @@ import com.practice.course_registration.domain.subject.service.SubjectQueryService; import com.practice.course_registration.domain.subject.service.SubjectService; import com.practice.course_registration.global.apiPayload.exception.handler.ErrorHandler; -import com.practice.course_registration.global.kafka.KafkaProducer; import com.practice.course_registration.global.redis.service.WaitQueueService; import com.practice.course_registration.global.redis.utils.RedisKeyUtils; import com.practice.course_registration.global.security.utils.SecurityUtils; diff --git a/src/main/java/com/practice/course_registration/domain/subject/domain/LikeSubject.java b/src/main/java/com/practice/course_registration/domain/subject/domain/LikeSubject.java index f1147ea..0c63689 100644 --- a/src/main/java/com/practice/course_registration/domain/subject/domain/LikeSubject.java +++ b/src/main/java/com/practice/course_registration/domain/subject/domain/LikeSubject.java @@ -1,6 +1,5 @@ package com.practice.course_registration.domain.subject.domain; -import com.practice.course_registration.domain.member.domain.Member; import com.practice.course_registration.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; @@ -16,13 +15,12 @@ public class LikeSubject extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") - private Member member; + // FK 연관 없이 ID로만 참조 + @Column(name = "member_id", nullable = false) + private Long memberId; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "subject_id") - private Subject subject; + @Column(name = "subject_id", nullable = false) + private Long subjectId; @Column(nullable = false) private Boolean isRegistration; diff --git a/src/main/java/com/practice/course_registration/domain/subject/domain/MemberSubject.java b/src/main/java/com/practice/course_registration/domain/subject/domain/MemberSubject.java index 92fed89..7827ed9 100644 --- a/src/main/java/com/practice/course_registration/domain/subject/domain/MemberSubject.java +++ b/src/main/java/com/practice/course_registration/domain/subject/domain/MemberSubject.java @@ -1,6 +1,5 @@ package com.practice.course_registration.domain.subject.domain; -import com.practice.course_registration.domain.member.domain.Member; import com.practice.course_registration.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; @@ -16,12 +15,11 @@ public class MemberSubject extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "member_id") - private Member member; + // FK 연관 없이 ID로만 참조 + @Column(name = "member_id", nullable = false) + private Long memberId; - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "subject_id") - private Subject subject; + @Column(name = "subject_id", nullable = false) + private Long subjectId; } diff --git a/src/main/java/com/practice/course_registration/domain/subject/domain/Subject.java b/src/main/java/com/practice/course_registration/domain/subject/domain/Subject.java index ac43d30..658d722 100644 --- a/src/main/java/com/practice/course_registration/domain/subject/domain/Subject.java +++ b/src/main/java/com/practice/course_registration/domain/subject/domain/Subject.java @@ -4,11 +4,8 @@ import com.practice.course_registration.global.enums.SubjectDay; import jakarta.persistence.*; import lombok.*; -import org.springframework.boot.context.properties.bind.DefaultValue; import java.time.LocalTime; -import java.util.ArrayList; -import java.util.List; @Entity @AllArgsConstructor @@ -43,15 +40,6 @@ public class Subject extends BaseEntity { private LocalTime endTime; - @OneToMany(mappedBy = "subject", cascade = CascadeType.ALL) - @Builder.Default - private List memberSubjects = new ArrayList<>(); - - @OneToMany(mappedBy = "subject", cascade = CascadeType.ALL) - @Builder.Default - private List likeSubjects = new ArrayList<>(); - - public boolean conflictCheck(Subject subject) { return this.getStartTime().isBefore(subject.getEndTime()) && this.getEndTime().isAfter(subject.getStartTime()); diff --git a/src/main/java/com/practice/course_registration/domain/subject/repository/LikeSubjectRepository.java b/src/main/java/com/practice/course_registration/domain/subject/repository/LikeSubjectRepository.java index 7a744e3..90d682a 100644 --- a/src/main/java/com/practice/course_registration/domain/subject/repository/LikeSubjectRepository.java +++ b/src/main/java/com/practice/course_registration/domain/subject/repository/LikeSubjectRepository.java @@ -1,36 +1,38 @@ -package com.practice.course_registration.domain.subject.repository; - -import com.practice.course_registration.domain.member.domain.Member; -import com.practice.course_registration.domain.subject.domain.LikeSubject; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.List; -import java.util.Optional; -import java.util.Set; - -public interface LikeSubjectRepository extends JpaRepository { - - @Query(""" - SELECT ls.subject.id - FROM LikeSubject ls - WHERE ls.member = :member AND ls.subject.id IN :subjectIds - """) - Set findAllByMemberAndSubject(@Param("member") Member member, @Param("subjectIds") List subjectIds); - - - List findByMemberId(Long memberId); - - @Query("SELECT ls FROM LikeSubject ls JOIN FETCH ls.subject s WHERE ls.member.id = :memberId ORDER BY s.subjectName") - Page findByMemberIdOrderBySubjectAsc(@Param("memberId") Long memberId, Pageable pageable); - - @Modifying - @Query("DELETE FROM LikeSubject ls WHERE ls.member.id = :memberId AND ls.subject.id = :subjectId") - void deleteByMemberIdAndSubjectId(@Param("memberId") Long memberId, @Param("subjectId") Long subjectId); - - Optional findByMemberIdAndSubjectId(Long memberId, Long subjectId); -} +package com.practice.course_registration.domain.subject.repository; + +import com.practice.course_registration.domain.subject.domain.LikeSubject; +import com.practice.course_registration.domain.subject.domain.Subject; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public interface LikeSubjectRepository extends JpaRepository { + + @Query(""" + SELECT ls.subjectId + FROM LikeSubject ls + WHERE ls.memberId = :memberId AND ls.subjectId IN :subjectIds + """) + Set findAllByMemberIdAndSubject(@Param("memberId") Long memberId, @Param("subjectIds") List subjectIds); + + + List findByMemberId(Long memberId); + + // 연관관계 없이 id로 조인하여 찜한 과목을 과목명순으로 조회 + @Query(value = "SELECT s FROM Subject s, LikeSubject ls WHERE ls.subjectId = s.id AND ls.memberId = :memberId ORDER BY s.subjectName", + countQuery = "SELECT count(ls) FROM LikeSubject ls WHERE ls.memberId = :memberId") + Page findLikedSubjectsByMemberIdOrderByName(@Param("memberId") Long memberId, Pageable pageable); + + @Modifying + @Query("DELETE FROM LikeSubject ls WHERE ls.memberId = :memberId AND ls.subjectId = :subjectId") + void deleteByMemberIdAndSubjectId(@Param("memberId") Long memberId, @Param("subjectId") Long subjectId); + + Optional findByMemberIdAndSubjectId(Long memberId, Long subjectId); +} diff --git a/src/main/java/com/practice/course_registration/domain/subject/repository/MemberSubjectRepository.java b/src/main/java/com/practice/course_registration/domain/subject/repository/MemberSubjectRepository.java index fe02839..405c51f 100644 --- a/src/main/java/com/practice/course_registration/domain/subject/repository/MemberSubjectRepository.java +++ b/src/main/java/com/practice/course_registration/domain/subject/repository/MemberSubjectRepository.java @@ -1,34 +1,30 @@ -package com.practice.course_registration.domain.subject.repository; - -import com.practice.course_registration.domain.member.domain.Member; -import com.practice.course_registration.domain.subject.domain.MemberSubject; -import com.practice.course_registration.domain.subject.domain.Subject; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.List; -import java.util.Optional; -import java.util.Set; - -public interface MemberSubjectRepository extends JpaRepository { - - @Query(""" - SELECT ms.subject.id - FROM MemberSubject ms - WHERE ms.member = :member AND ms.subject.id IN :subjectIds - """) - Set findAllIdByMemberAndSubject(@Param("member") Member member, @Param("subjectIds") List subjectIds); - - Optional findByMemberAndSubject(Member member, Subject subject); - - Optional findByMemberIdAndSubjectId(Long memberId, Long subjectId); - - List findAllByMember(Member member); - - @Modifying - @Query("delete from MemberSubject ms where ms.member.id = :memberId and ms.subject.id = :subjectId") - void deleteByMemberIdAndSubjectId(@Param("memberId") Long memberId, @Param("subjectId") Long subjectId); - -} +package com.practice.course_registration.domain.subject.repository; + +import com.practice.course_registration.domain.subject.domain.MemberSubject; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public interface MemberSubjectRepository extends JpaRepository { + + @Query(""" + SELECT ms.subjectId + FROM MemberSubject ms + WHERE ms.memberId = :memberId AND ms.subjectId IN :subjectIds + """) + Set findAllIdByMemberIdAndSubject(@Param("memberId") Long memberId, @Param("subjectIds") List subjectIds); + + Optional findByMemberIdAndSubjectId(Long memberId, Long subjectId); + + List findAllByMemberId(Long memberId); + + @Modifying + @Query("delete from MemberSubject ms where ms.memberId = :memberId and ms.subjectId = :subjectId") + void deleteByMemberIdAndSubjectId(@Param("memberId") Long memberId, @Param("subjectId") Long subjectId); + +} diff --git a/src/main/java/com/practice/course_registration/domain/subject/service/CourseEnrollmentService.java b/src/main/java/com/practice/course_registration/domain/subject/service/CourseEnrollmentService.java index f5050a7..5702d7e 100644 --- a/src/main/java/com/practice/course_registration/domain/subject/service/CourseEnrollmentService.java +++ b/src/main/java/com/practice/course_registration/domain/subject/service/CourseEnrollmentService.java @@ -14,6 +14,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @RequiredArgsConstructor @Slf4j @@ -34,7 +36,7 @@ public void processEnrollment(Long subjectId, Long memberId) { // 해당 과목 찾기 Subject subject = findSubjectById(subjectId); - if (memberSubjectRepository.findByMemberAndSubject(member, subject).isPresent()) { + if (memberSubjectRepository.findByMemberIdAndSubjectId(memberId, subjectId).isPresent()) { throw new ErrorHandler(ErrorStatus.ALREADY_APPLY_SUBJECT); } @@ -42,8 +44,13 @@ public void processEnrollment(Long subjectId, Long memberId) { throw new ErrorHandler(ErrorStatus.OVER_SOCRE_POSSIBLE); } - int isSameCode = member.getMemberSubjects().stream() - .map(MemberSubject::getSubject) + // 연관관계 탐색 대신 memberId로 신청 내역 조회 후 과목을 id로 조회 + List enrolledSubjectIds = memberSubjectRepository.findAllByMemberId(memberId).stream() + .map(MemberSubject::getSubjectId) + .toList(); + List enrolledSubjects = subjectRepository.findAllById(enrolledSubjectIds); + + int isSameCode = enrolledSubjects.stream() .filter(subj -> subj.getCode().equals(subject.getCode()) ) @@ -56,8 +63,7 @@ public void processEnrollment(Long subjectId, Long memberId) { throw new ErrorHandler(ErrorStatus.ALREADY_APPLY_SUBJECT); } - boolean conflict = member.getMemberSubjects().stream() - .map(MemberSubject::getSubject) + boolean conflict = enrolledSubjects.stream() .filter(subj -> subj.getSubjectDay() == subject.getSubjectDay() ) @@ -81,13 +87,11 @@ public void processEnrollment(Long subjectId, Long memberId) { // 저장 MemberSubject memberSubject = MemberSubject.builder() - .member(member) - .subject(subject) + .memberId(memberId) + .subjectId(subject.getId()) .build(); memberSubjectRepository.save(memberSubject); - member.getMemberSubjects().add(memberSubject); - subject.getMemberSubjects().add(memberSubject); // redis hold key 삭제 luaRepository.deleteHoldKeyOnly(subject.getId(), memberId); diff --git a/src/main/java/com/practice/course_registration/domain/subject/service/LikeSubjectService.java b/src/main/java/com/practice/course_registration/domain/subject/service/LikeSubjectService.java index 42a8f3a..990b350 100644 --- a/src/main/java/com/practice/course_registration/domain/subject/service/LikeSubjectService.java +++ b/src/main/java/com/practice/course_registration/domain/subject/service/LikeSubjectService.java @@ -1,7 +1,5 @@ package com.practice.course_registration.domain.subject.service; -import com.practice.course_registration.domain.member.domain.Member; -import com.practice.course_registration.domain.member.repository.MemberRepository; import com.practice.course_registration.domain.subject.domain.LikeSubject; import com.practice.course_registration.domain.subject.domain.Subject; import com.practice.course_registration.domain.subject.dto.LikeSubjectDTO; @@ -27,16 +25,13 @@ public class LikeSubjectService { private final LikeSubjectRepository likeSubjectRepository; private final MemberSubjectRepository memberSubjectRepository; private final SubjectRepository subjectRepository; - private final MemberRepository memberRepository; public LikeSubjectService(LikeSubjectRepository likeSubjectRepository, MemberSubjectRepository memberSubjectRepository, - SubjectRepository subjectRepository, - MemberRepository memberRepository) { + SubjectRepository subjectRepository) { this.likeSubjectRepository = likeSubjectRepository; this.memberSubjectRepository = memberSubjectRepository; this.subjectRepository = subjectRepository; - this.memberRepository = memberRepository; } @Transactional @@ -45,10 +40,6 @@ public void addLikeSubject(Long memberId, String code) { Subject subject = subjectRepository.findByCode(code) .orElseThrow(() -> new ErrorHandler(ErrorStatus.SUBJECT_NOT_FOUND)); - // Member 찾기 - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new ErrorHandler(ErrorStatus.MEMBER_NOT_FOUND)); - // 이미 희망과목에 있는지 확인 if (likeSubjectRepository.findByMemberIdAndSubjectId(memberId, subject.getId()).isPresent()) { throw new ErrorHandler(ErrorStatus.ALREADY_APPLY_SUBJECT); @@ -56,8 +47,8 @@ public void addLikeSubject(Long memberId, String code) { // LikeSubject 생성 및 저장 LikeSubject likeSubject = LikeSubject.builder() - .member(member) - .subject(subject) + .memberId(memberId) + .subjectId(subject.getId()) .isRegistration(false) .build(); @@ -76,10 +67,9 @@ public void removeLikeSubject(Long memberId, String code) { public Page getLikeSubjectsByUserId(Long userId, int page, int size) { Pageable pageable = PageRequest.of(page, size); - // likeSubjectPage : 여러 개의 Subject 객체를 페이지 단위로 가지고 있다. + // 연관관계 없이 id 조인으로 찜한 Subject를 과목명순 페이지 조회 Page likeSubjectPage = likeSubjectRepository - .findByMemberIdOrderBySubjectAsc(userId, pageable) - .map(LikeSubject::getSubject); + .findLikedSubjectsByMemberIdOrderByName(userId, pageable); return likeSubjectPage.map(subject -> { // 해당 과목을 사용자가 수강신청했는지 확인 diff --git a/src/main/java/com/practice/course_registration/domain/subject/service/SubjectQueryService.java b/src/main/java/com/practice/course_registration/domain/subject/service/SubjectQueryService.java index 4360d12..6ecd719 100644 --- a/src/main/java/com/practice/course_registration/domain/subject/service/SubjectQueryService.java +++ b/src/main/java/com/practice/course_registration/domain/subject/service/SubjectQueryService.java @@ -1,7 +1,5 @@ package com.practice.course_registration.domain.subject.service; -import com.practice.course_registration.domain.member.domain.Member; -import com.practice.course_registration.domain.member.repository.MemberRepository; import com.practice.course_registration.domain.subject.domain.MemberSubject; import com.practice.course_registration.domain.subject.domain.Subject; import com.practice.course_registration.domain.subject.dto.CourseFilterRequestDTO; @@ -11,8 +9,6 @@ import com.practice.course_registration.domain.subject.repository.LikeSubjectRepository; import com.practice.course_registration.domain.subject.repository.MemberSubjectRepository; import com.practice.course_registration.domain.subject.repository.SubjectRepository; -import com.practice.course_registration.global.apiPayload.code.status.ErrorStatus; -import com.practice.course_registration.global.apiPayload.exception.handler.ErrorHandler; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -33,14 +29,12 @@ public class SubjectQueryService { private final SubjectRepository subjectRepository; - private final MemberRepository memberRepository; private final MemberSubjectRepository memberSubjectRepository; private final LikeSubjectRepository likeSubjectRepository; public Page searchAllSubject(Long memberId, CourseFilterRequestDTO filters, Pageable pageable) { log.info("===========search 시작=============="); - Member member = findById(memberId); String code = nullIfBlank(filters.getCode()); String professorName = nullIfBlank(filters.getProfessorName()); @@ -62,8 +56,8 @@ public Page searchAllSubject(Long memberId, CourseFilterRequ .toList(); log.info("===========페이지 크기 : " + subjectIds.size()); - Set registeredIds = memberSubjectRepository.findAllIdByMemberAndSubject(member, subjectIds); - Set likedIds = likeSubjectRepository.findAllByMemberAndSubject(member, subjectIds); + Set registeredIds = memberSubjectRepository.findAllIdByMemberIdAndSubject(memberId, subjectIds); + Set likedIds = likeSubjectRepository.findAllByMemberIdAndSubject(memberId, subjectIds); return subjects.map(subject -> SubjectResponseDTO.builder() .subjectName(subject.getSubjectName()) @@ -83,11 +77,11 @@ public Page searchAllSubject(Long memberId, CourseFilterRequ public List searchMySubject(Long memberId) { - Member member = findById(memberId); - - List memberSubjects = memberSubjectRepository.findAllByMember(member); - return memberSubjects.stream() - .map(MemberSubject::getSubject) + List subjectIds = memberSubjectRepository.findAllByMemberId(memberId).stream() + .map(MemberSubject::getSubjectId) + .toList(); + List subjects = subjectRepository.findAllById(subjectIds); + return subjects.stream() .map(subject -> MyRegisteredSubjectResponseDTO.builder() .id(subject.getId()) .subjectName(subject.getSubjectName()) @@ -104,10 +98,6 @@ public List searchMySubject(Long memberId) { .collect(Collectors.toList()); } - private Member findById(Long memberId) { - return memberRepository.findById(memberId).orElseThrow(() -> new ErrorHandler(ErrorStatus.MEMBER_NOT_FOUND)); - } - private String nullIfBlank(String s) { return (s == null || s.isBlank()) ? null : s.trim(); } diff --git a/src/main/java/com/practice/course_registration/domain/subject/service/SubjectService.java b/src/main/java/com/practice/course_registration/domain/subject/service/SubjectService.java index 0683025..b82d608 100644 --- a/src/main/java/com/practice/course_registration/domain/subject/service/SubjectService.java +++ b/src/main/java/com/practice/course_registration/domain/subject/service/SubjectService.java @@ -13,6 +13,7 @@ import com.practice.course_registration.global.redis.service.IdempotencyService; import com.practice.course_registration.global.redis.service.WaitQueueService; import java.time.Duration; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -112,13 +113,11 @@ public void applyCourseWithToken(Long memberId, String code) { } MemberSubject memberSubject = MemberSubject.builder() - .member(member) - .subject(subject) + .memberId(memberId) + .subjectId(subject.getId()) .build() ; memberSubjectRepository.save(memberSubject); - member.getMemberSubjects().add(memberSubject); - subject.getMemberSubjects().add(memberSubject); member.addScore(subject.getScore()); luaRepository.deleteHoldKeyOnly(subject.getId(), memberId); // hold 정리 @@ -140,7 +139,7 @@ public void applyCourseWithToken(Long memberId, String code) { * - 신청가능학점을 넘긴경우 -> 위 코드에서 lua 결과로 판단 * */ private void validateCheck(Member member, Subject subject) { - if (memberSubjectRepository.findByMemberAndSubject(member, subject).isPresent()) { + if (memberSubjectRepository.findByMemberIdAndSubjectId(member.getId(), subject.getId()).isPresent()) { throw new ErrorHandler(ErrorStatus.ALREADY_APPLY_SUBJECT); } @@ -148,16 +147,20 @@ private void validateCheck(Member member, Subject subject) { throw new ErrorHandler(ErrorStatus.OVER_SOCRE_POSSIBLE); } - boolean conflict = member.getMemberSubjects().stream() - .map(MemberSubject::getSubject) + // 연관관계 탐색 대신 memberId로 신청 내역 조회 후 과목을 id로 조회 + List enrolledSubjectIds = memberSubjectRepository.findAllByMemberId(member.getId()).stream() + .map(MemberSubject::getSubjectId) + .toList(); + List enrolledSubjects = subjectRepository.findAllById(enrolledSubjectIds); + + boolean conflict = enrolledSubjects.stream() .filter(subj -> subj.getSubjectDay() == subject.getSubjectDay() ) .anyMatch(subj -> subj.conflictCheck(subject)) ; - int isSameCode = member.getMemberSubjects().stream() - .map(MemberSubject::getSubject) + int isSameCode = enrolledSubjects.stream() .filter(subj -> subj.getCode().equals(subject.getCode()) ) @@ -196,7 +199,7 @@ public void cancelCourse(Long memberId, Long subjectId) { // 해당 과목 찾기 Subject subject = findSubjectById(subjectId); - MemberSubject memberSubject = memberSubjectRepository.findByMemberAndSubject(member, subject) + memberSubjectRepository.findByMemberIdAndSubjectId(memberId, subjectId) .orElseThrow(() -> new ErrorHandler(ErrorStatus.NOT_APPLY_SUBJECT)); // 신청 학점 줄이기 @@ -217,7 +220,7 @@ private Subject findSubjectById(Long subjectId) { private Member findMemberById(Long memberId) { - return memberRepository.findWithSubjectsById(memberId).orElseThrow(() -> new ErrorHandler(ErrorStatus.MEMBER_NOT_FOUND)); + return memberRepository.findById(memberId).orElseThrow(() -> new ErrorHandler(ErrorStatus.MEMBER_NOT_FOUND)); } private Subject findByCode(String code) { diff --git a/src/main/java/com/practice/course_registration/global/config/KafkaProducerConfig.java b/src/main/java/com/practice/course_registration/global/config/KafkaProducerConfig.java deleted file mode 100644 index 3d96fd0..0000000 --- a/src/main/java/com/practice/course_registration/global/config/KafkaProducerConfig.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.practice.course_registration.global.config; - -import com.practice.course_registration.global.kafka.RegistrationMessage; -import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.admin.NewTopic; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.common.serialization.StringSerializer; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.config.TopicBuilder; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.core.ProducerFactory; -import org.springframework.kafka.support.serializer.JsonSerializer; - -import java.util.HashMap; -import java.util.Map; - -@Slf4j -public class KafkaProducerConfig { - @Value("${spring.kafka.bootstrap-servers}") - private String bootstrapServers; - - @Bean - public ProducerFactory producerFactory(){ - return new DefaultKafkaProducerFactory<>(producerConfigs()); - } - - @Bean - public Map producerConfigs(){ - // configs 안에 다양한 타입이 들어가기 때문에, Object를 통해 모두 담는다. - Map props = new HashMap<>(); - props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); - props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); - props.put(ProducerConfig.ACKS_CONFIG, "all"); - props.put(ProducerConfig.RETRIES_CONFIG, 3); - - return props; - } - - @Bean - public KafkaTemplate kafkaTemplate(){ - return new KafkaTemplate<>(producerFactory()); - } -} diff --git a/src/main/java/com/practice/course_registration/global/kafka/KafkaProducer.java b/src/main/java/com/practice/course_registration/global/kafka/KafkaProducer.java deleted file mode 100644 index 857d8e2..0000000 --- a/src/main/java/com/practice/course_registration/global/kafka/KafkaProducer.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.practice.course_registration.global.kafka; - -import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.common.errors.RetriableException; -import org.apache.kafka.common.errors.SerializationException; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.support.SendResult; -import org.springframework.stereotype.Component; - -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -@Component -@Slf4j -public class KafkaProducer { - private final KafkaTemplate kafkaTemplate; - private final String targetTopic = "registration-queue"; - - public KafkaProducer(KafkaTemplate kafkaTemplate) { - this.kafkaTemplate = kafkaTemplate; - } - - public void create(Long userId, String subjectCode){ - String requestId = UUID.randomUUID().toString(); - - RegistrationMessage message = new RegistrationMessage( - userId, - subjectCode, - System.currentTimeMillis(), - requestId - ); - - // 비동기로 카프카에 전송 - try{ - CompletableFuture> future = - kafkaTemplate.send(targetTopic, subjectCode, message); - - future.whenCompleteAsync((result, ex) -> { - if (ex == null){ - onSuccess(result, message, requestId); - } - else { - // 비동기 실패 - handleException(ex, message); - } - }); - } catch (Exception ex){ - // 동기 실패 - handleException(ex, message); - } - } - - private void onSuccess(final SendResult result, final T t, String requestId){ - log.info("성공적인 메시지 =[{}] request-id= {} topic-partition={}-{} offset={}", - t, - requestId, - result.getRecordMetadata().topic(), - result.getRecordMetadata().partition(), - result.getRecordMetadata().offset()); - } - - // Message를 발급하는 과정에서 생긴 오류 핸들링 - private void handleException(Throwable ex, RegistrationMessage message) { - // cause가 있으면 cause를, 없으면 ex 자체를 확인 - Throwable cause = ex.getCause() != null ? ex.getCause() : ex; - - if (cause instanceof RetriableException){ - log.warn("Retriable Exception이 발생하였습니다. 자동으로 다시 시도합니다. {}", cause.getMessage()); - } else if (cause instanceof SerializationException){ - log.error("Serialization 오류입니다. DLT로 전송합니다. {}", message); - sendToDeadLetterTopic(message, targetTopic); - } else { - log.error("{} 메시지를 dead-letter queue로 전송합니다. 에러는 다음과 같습니다. {}", message, cause.getMessage()); - sendToDeadLetterTopic(message, targetTopic); - } - } - - private void sendToDeadLetterTopic(T message, final String topic){ - String deadLetterTopic = topic + ".DLT"; - log.info("DLT로 메시지를 보냅니다 : {}", deadLetterTopic); - kafkaTemplate.send(deadLetterTopic, (RegistrationMessage) message); - } -} diff --git a/src/main/java/com/practice/course_registration/global/kafka/RegistrationMessage.java b/src/main/java/com/practice/course_registration/global/kafka/RegistrationMessage.java deleted file mode 100644 index 08e7e06..0000000 --- a/src/main/java/com/practice/course_registration/global/kafka/RegistrationMessage.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.practice.course_registration.global.kafka; - -import lombok.*; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -@ToString -public class RegistrationMessage { - // 기본 정보 - private Long userId; - private String courseId; - private Long timestamp; - private String requestId; -} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index fb0c32f..57ceef1 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -34,9 +34,6 @@ spring: resources: add-mappings: false - kafka: - bootstrap-servers: ${SPRING_KAFKA_BOOTSTRAP_SERVERS} - server: servlet: session: diff --git a/src/test/java/com/practice/course_registration/CourseRegistrationApplicationTests.java b/src/test/java/com/practice/course_registration/CourseRegistrationApplicationTests.java index 955cbe1..79254d6 100644 --- a/src/test/java/com/practice/course_registration/CourseRegistrationApplicationTests.java +++ b/src/test/java/com/practice/course_registration/CourseRegistrationApplicationTests.java @@ -1,18 +1,9 @@ package com.practice.course_registration; -import com.practice.course_registration.global.kafka.KafkaProducer; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.test.context.bean.override.mockito.MockitoBean; @SpringBootTest -@EmbeddedKafka(partitions = 1, brokerProperties = {"listeners=PLAINTEXT://localhost:9092", - "port=9092"}) class CourseRegistrationApplicationTests { @Test void contextLoads() { diff --git a/src/test/java/com/practice/course_registration/KafkaProducerTest.java b/src/test/java/com/practice/course_registration/KafkaProducerTest.java deleted file mode 100644 index 1f60541..0000000 --- a/src/test/java/com/practice/course_registration/KafkaProducerTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.practice.course_registration; - -import com.practice.course_registration.global.kafka.KafkaProducer; -import com.practice.course_registration.global.kafka.RegistrationMessage; -import org.apache.kafka.common.errors.TimeoutException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.kafka.core.KafkaTemplate; -import java.util.concurrent.CompletableFuture; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -public class KafkaProducerTest { - @Mock - private KafkaTemplate kafkaTemplate; - @InjectMocks - KafkaProducer kafkaProducer; - - - @Test - @DisplayName("예외 발생 시 DLT로 전송한다.") - void ShouldSendToDLT() { - // given - when(kafkaTemplate.send(anyString(), anyString(), any())) - .thenReturn(CompletableFuture.failedFuture( - new RuntimeException("브로커 이용 불가") - )); - - // when - kafkaProducer.create(1L, "TEST1"); - - // then - verify(kafkaTemplate, timeout(1000)) - .send(eq("registration-queue.DLT"), any(RegistrationMessage.class)); - } - - - @Test - @DisplayName("RetriableException은 DLT로 안보낸다") - void ShouldNotSendToDLT() { - // given - when(kafkaTemplate.send(anyString(), anyString(), any())) - .thenReturn(CompletableFuture.failedFuture( - new TimeoutException("Temporary failure") - )); - - // when - kafkaProducer.create(1L, "TEST1"); - - // then - verify(kafkaTemplate, times(1)) - .send(eq("registration-queue"), anyString(), any()); - - verify(kafkaTemplate, never()) - .send(eq("registration-queue.DLT"), any()); - - } -}