[문제]
채용 공고에 있는 기술 스택(Tech Stack)을 키워드(Keyword)로 몇 개 넣은 다음, 사용자가 알림 받고자 하는 채용 키워드를 등록하는 API를 구현해서 Postman을 실행했더니 오류가 발생했다.
'Duplicate entry '1-3' for key 'user_keyword'
// (a) 'user_keyword'라는 고유 키에 '1-3' 값이 중복되어 삽입됨
// (b) 1: user_id 값
// (c) 3: keyword_id 값
[원인 및 해결 과정]
package com.project.cheerha.domain.keyword.entity;
import com.project.cheerha.domain.user.entity.User;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@NoArgsConstructor
@Table(name = "user_keyword", uniqueConstraints = {
@UniqueConstraint(columnNames = {"user_id", "keyword_id"})}
)
public class UserKeyword {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "keyword_id")
private Keyword keyword;
public static UserKeyword of(
User user,
Keyword keyword
) {
UserKeyword userKeyword = new UserKeyword();
userKeyword.user = user;
userKeyword.keyword = keyword;
return userKeyword;
}
}
원인은 오류 메시지로 금방 찾을 수 있었다. SQL Error가 발생했다는 말은 데이터베이스(database)와 관련된 문제라는 뜻이고, 데이터베이스에는 엔티티(Entity)가 저장되므로 가장 먼저 중간 테이블인 UserKeyword 클래스(class)를 살폈다. 짐작대로 'uniqueConstraints' 속성을 설정했지만, 중복 검증 로직을 빠뜨려서 오류가 발생했다.
@Table 어노테이션(annotation)에서 'uniqueConstraints' 속성을 사용하여 사용자 식별자(user_id)와 키워드 식별자(keyword_id)에 복합 고유 키 제약 조건을 설정했다. 이 제약 조건 덕분에 user_id와 keyword_id 조합은 데이터베이스 중복으로 저장되지 않았고, 사용자가 이미 구독 중인 키워드를 추가하려고 하면 'Duplicate entry' 오류가 발생했다. 이 문제를 해결하려면, 키워드를 저장하기 전에 해당 user_id와 keyword_id 조합이 이미 존재하는지 확인하는 로직을 추가해야 했다.
(1) UserKeywordRepository에 메서드(method) 추가하기 ▼
package com.project.cheerha.domain.keyword.repository;
import com.project.cheerha.domain.keyword.entity.Keyword;
import com.project.cheerha.domain.keyword.entity.UserKeyword;
import com.project.cheerha.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserKeywordRepository extends JpaRepository<UserKeyword, Long> {
// 주어진 User와 Keyword의 조합이 존재하는지 확인하는 메서드
boolean existsByUserAndKeyword (User user, Keyword keyword);
}
(2) (1)에서 작성한 메서드를 UserKeywordService에서 호출하기 ▼
package com.project.cheerha.domain.keyword.service;
import com.project.cheerha.common.dto.AuthUser;
import com.project.cheerha.common.exception.CustomException;
import com.project.cheerha.common.exception.ErrorCode;
import com.project.cheerha.domain.keyword.dto.request.CreateUserKeywordRequestDto;
import com.project.cheerha.domain.keyword.dto.response.CreateUserKeywordResponseDto;
import com.project.cheerha.domain.keyword.entity.Keyword;
import com.project.cheerha.domain.keyword.entity.UserKeyword;
import com.project.cheerha.domain.keyword.repository.KeywordRepository;
import com.project.cheerha.domain.keyword.repository.UserKeywordRepository;
import com.project.cheerha.domain.user.entity.User;
import com.project.cheerha.domain.user.repository.UserRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class UserKeywordService {
private final KeywordRepository keywordRepository;
private final UserKeywordRepository userKeywordRepository;
private final UserRepository userRepository;
@Transactional
public CreateUserKeywordResponseDto createUserKeyword(
AuthUser authUser,
CreateUserKeywordRequestDto requestDto
) {
Long userId = authUser.id();
User foundUser = findUserById(userId);
// 요청 DTO에서 키워드 식별자 목록을 가져옴
List<Long> keywordIdList = requestDto.keywordIdList();
keywordIdList.forEach(
keywordId -> {
// 식별자로 키워드 엔티티 조회
Keyword foundKeyword = findKeywordById(keywordId);
// [1/2]에서 작성한 메서드 호출
boolean isKeywordAlreadyChosen = userKeywordRepository.existsByUserAndKeyword(
foundUser,
foundKeyword
);
if (isKeywordAlreadyChosen) {
throw new CustomException(ErrorCode.KEYWORD_ALREADY_CHOSEN);
}
// 중간 테이블인 UserKeyword 엔티티 생성
UserKeyword newUserKeyword = UserKeyword.of(
foundUser,
foundKeyword
);
// 생성된 UserKeyword를 데이터베이스에 저장
userKeywordRepository.save(newUserKeyword);
}
);
return CreateUserKeywordResponseDto.of(keywordIdList);
}
// 주어진 userId로 사용자 엔티티를 조회하는
// 존재하지 않으면 USER_NOT_FOUND 예외 발생
private User findUserById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
}
// 주어진 keywordId로 키워드 엔티티를 조회하는 메서드
// 존재하지 않으면 KEYWORD_NOT_FOUND 예외 발생
private Keyword findKeywordById(Long keywordId) {
return keywordRepository.findById(keywordId)
.orElseThrow(() -> new CustomException(ErrorCode.KEYWORD_NOT_FOUND));
}
}
1차로 수정한 다음 다시 확인했을 때 예외 처리가 잘 이루어졌다.
이때 문득 의문이 들었다.
'굳이 예외 처리를 해야 할까?'
'기존에 등록한 키워드가 있으면, 바로 응답 DTO를 보내줘도 되지 않을까?'
'그렇게 하면 비즈니스 로직을 더 간결하게 다듬을 수 있을 텐데?'
앞으로 프로젝트에 새로운 기능이 추가되는 만큼, 최소 기능 제품을 만들 때는 코드가 간결할수록 유지 보수가 쉬울 듯했다. 이런 이유로 예외 처리를 별도로 하지 않고 if문을 활용하여 로직을 수정했다. ▼
package com.project.cheerha.domain.keyword.service;
import com.project.cheerha.common.dto.AuthUser;
import com.project.cheerha.common.exception.CustomException;
import com.project.cheerha.common.exception.ErrorCode;
import com.project.cheerha.domain.keyword.dto.request.CreateUserKeywordRequestDto;
import com.project.cheerha.domain.keyword.dto.response.CreateUserKeywordResponseDto;
import com.project.cheerha.domain.keyword.entity.Keyword;
import com.project.cheerha.domain.keyword.entity.UserKeyword;
import com.project.cheerha.domain.keyword.repository.KeywordRepository;
import com.project.cheerha.domain.keyword.repository.UserKeywordRepository;
import com.project.cheerha.domain.user.entity.User;
import com.project.cheerha.domain.user.repository.UserRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class UserKeywordService {
private final KeywordRepository keywordRepository;
private final UserKeywordRepository userKeywordRepository;
private final UserRepository userRepository;
@Transactional
public CreateUserKeywordResponseDto createUserKeyword(
AuthUser authUser,
CreateUserKeywordRequestDto requestDto
) {
Long userId = authUser.id();
User foundUser = findUserById(userId);
List<Long> keywordIdList = requestDto.keywordIdList();
keywordIdList.forEach(
keywordId -> {
Keyword foundKeyword = findKeywordById(keywordId);
boolean isKeywordAlreadyChosen = userKeywordRepository.existsByUserAndKeyword(
foundUser,
foundKeyword
);
if (!isKeywordAlreadyChosen) {
UserKeyword newUserKeyword = UserKeyword.of(
foundUser,
foundKeyword
);
userKeywordRepository.save(newUserKeyword);
}
}
);
return CreateUserKeywordResponseDto.of(keywordIdList);
}
private User findUserById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
}
private Keyword findKeywordById(Long keywordId) {
return keywordRepository.findById(keywordId)
.orElseThrow(() -> new CustomException(ErrorCode.KEYWORD_NOT_FOUND));
}
}
2차로 수정한 다음 Postman을 실행하여 데이터베이스에 저장된 키워드와 똑같이 키워드 Id를 입력한 결과, 별다른 예외 없이 '201 Created' 상태 메시지와 응답 DTO가 반환되었다. ▼
두 번째 테스트로 새로운 키워드 Id를 입력했을 때도 문제없이 데이터베이스에 저장되고 첫 번째 테스트와 동일한 결과가 나왔다. ▼
세 번째로는 데이터베이스에 이미 저장된 키워드 식별자와 아직 저장되지 않은 식별자를 섞어서 요청을 보냈고, 데이터베이스에 저장되지 않은 키워드 식별자만 새롭게 저장된 점을 확인할 수 있었다. ▼
이때 새로운 질문이 머릿속에 맴돌았다.
'응답으로 키워드의 식별자만 전달하는 대신, 이름을 함께 반환하면 프론트엔드(Front-end)에서 더 편하게 사용할 수 있지 않을까?'
이런 이유로 DTO와 서비스 레이어(Service Layer)를 다시 수정했다. ▼
package com.project.cheerha.domain.keyword.dto.request;
import java.util.List;
public record CreateUserKeywordRequestDto(List<Long> keywordIdList) {
}
package com.project.cheerha.domain.keyword.dto.response;
import java.util.List;
public record CreateUserKeywordResponseDto(List<String> keywordList) {
public static CreateUserKeywordResponseDto of(List<String> keywordList) {
return new CreateUserKeywordResponseDto(keywordList);
}
}
package com.project.cheerha.domain.keyword.service;
import com.project.cheerha.common.dto.AuthUser;
import com.project.cheerha.common.exception.CustomException;
import com.project.cheerha.common.exception.ErrorCode;
import com.project.cheerha.domain.keyword.dto.request.CreateUserKeywordRequestDto;
import com.project.cheerha.domain.keyword.dto.response.CreateUserKeywordResponseDto;
import com.project.cheerha.domain.keyword.entity.Keyword;
import com.project.cheerha.domain.keyword.entity.UserKeyword;
import com.project.cheerha.domain.keyword.repository.KeywordRepository;
import com.project.cheerha.domain.keyword.repository.UserKeywordRepository;
import com.project.cheerha.domain.user.entity.User;
import com.project.cheerha.domain.user.repository.UserRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class UserKeywordService {
private final KeywordRepository keywordRepository;
private final UserKeywordRepository userKeywordRepository;
private final UserRepository userRepository;
@Transactional
public CreateUserKeywordResponseDto createUserKeyword(
AuthUser authUser,
CreateUserKeywordRequestDto requestDto
) {
Long userId = authUser.id();
User foundUser = findUserById(userId);
List<Long> keywordIdList = requestDto.keywordIdList();
List<String> keywordNameList = findKeywordNameListByIdList(keywordIdList);
saveUserKeywordIfNotExist(keywordIdList, foundUser);
return CreateUserKeywordResponseDto.of(keywordNameList);
}
// 키워드 식별자 목록으로 조회한 키워드 이름 목록을 반환하는 메서드
private List<String> findKeywordNameListByIdList(List<Long> keywordIdList) {
return keywordIdList.stream()
.map(this::findKeywordById)
.map(Keyword::getName)
.toList();
}
// 키워드 식별자 목록으로 UserKeyword 객체를 저장하는 메서드
private void saveUserKeywordIfNotExist(
List<Long> keywordIdList,
User foundUser
) {
keywordIdList.forEach(
keywordId -> {
Keyword foundKeyword = findKeywordById(keywordId);
boolean isKeywordAlreadyChosen = userKeywordRepository.existsByUserAndKeyword(
foundUser,
foundKeyword
);
if (!isKeywordAlreadyChosen) {
UserKeyword newUserKeyword = UserKeyword.of(
foundUser,
foundKeyword
);
userKeywordRepository.save(newUserKeyword);
}
}
);
}
private User findUserById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
}
private Keyword findKeywordById(Long keywordId) {
return keywordRepository.findById(keywordId)
.orElseThrow(() -> new CustomException(ErrorCode.KEYWORD_NOT_FOUND));
}
}
수정 후 Postman을 실행하니 원하는 대로 결과가 잘 나오긴 했는데……. ▼
'특정 식별자에 해당하는 키워드 객체를 굳이 두 번 조회해야 하나?' 고개가 갸우뚱했다. ▼
private Keyword findKeywordById(Long keywordId) {
return keywordRepository.findById(keywordId)
.orElseThrow(() -> new CustomException(ErrorCode.KEYWORD_NOT_FOUND));
}
불필요한 로직을 줄이고자 우선 메서드에 한 가지 동작만 있도록 수정했다. ▼
package com.project.cheerha.domain.keyword.service;
import com.project.cheerha.common.dto.AuthUser;
import com.project.cheerha.common.exception.CustomException;
import com.project.cheerha.common.exception.ErrorCode;
import com.project.cheerha.domain.keyword.dto.request.CreateUserKeywordRequestDto;
import com.project.cheerha.domain.keyword.dto.response.CreateUserKeywordResponseDto;
import com.project.cheerha.domain.keyword.entity.Keyword;
import com.project.cheerha.domain.keyword.entity.UserKeyword;
import com.project.cheerha.domain.keyword.repository.KeywordRepository;
import com.project.cheerha.domain.keyword.repository.UserKeywordRepository;
import com.project.cheerha.domain.user.entity.User;
import com.project.cheerha.domain.user.repository.UserRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class UserKeywordService {
private final KeywordRepository keywordRepository;
private final UserKeywordRepository userKeywordRepository;
private final UserRepository userRepository;
@Transactional
public CreateUserKeywordResponseDto createUserKeyword(
AuthUser authUser,
CreateUserKeywordRequestDto requestDto
) {
Long userId = authUser.id();
User foundUser = findUserById(userId);
List<Long> idList = requestDto.keywordIdList();
List<Keyword> keywordList = findKeywordListByIdList(idList);
createNewUserKeywordIfNotExist(keywordList, foundUser);
List<String> keywordNameList = extractKeywordNameList(keywordList);
return CreateUserKeywordResponseDto.of(keywordNameList);
}
private List<String> extractKeywordNameList(List<Keyword> keywordList) {
return keywordList.stream()
.map(Keyword::getName)
.toList();
}
private void createNewUserKeywordIfNotExist(
List<Keyword> keywordList,
User foundUser
) {
keywordList.forEach(
foundKeyword -> {
boolean isKeywordAlreadyChosen = userKeywordRepository.existsByUserAndKeyword(
foundUser,
foundKeyword
);
if (!isKeywordAlreadyChosen) {
UserKeyword newUserKeyword = UserKeyword.of(
foundUser,
foundKeyword
);
userKeywordRepository.save(newUserKeyword);
}
}
);
}
private List<Keyword> findKeywordListByIdList(List<Long> keywordIdList) {
return keywordIdList.stream()
.map(this::findKeywordById)
.toList();
}
private Keyword findKeywordById(Long keywordId) {
return keywordRepository.findById(keywordId)
.orElseThrow(() -> new CustomException(ErrorCode.KEYWORD_NOT_FOUND));
}
private User findUserById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
}
}
메서드를 잘게 쪼개고 나니, 데이터베이스와 소통하지 않는 메서드를 뽑아낼 수 있었다. 해당 메서드는 키워드 엔티티 내부로 옮겼다. ▼
package com.project.cheerha.domain.keyword.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@NoArgsConstructor
@Table(name = "keyword")
public class Keyword {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 50, nullable = false)
private String name;
// 키워드 객체 목록에서 이름을 추출하는 메서드
public static List<String> extractNameListFromEntityList(List<Keyword> keywordList) {
return keywordList.stream()
.map(Keyword::getName)
.toList();
}
}
package com.project.cheerha.domain.keyword.service;
import com.project.cheerha.common.dto.AuthUser;
import com.project.cheerha.common.exception.CustomException;
import com.project.cheerha.common.exception.ErrorCode;
import com.project.cheerha.domain.keyword.dto.request.CreateUserKeywordRequestDto;
import com.project.cheerha.domain.keyword.dto.response.CreateUserKeywordResponseDto;
import com.project.cheerha.domain.keyword.entity.Keyword;
import com.project.cheerha.domain.keyword.entity.UserKeyword;
import com.project.cheerha.domain.keyword.repository.KeywordRepository;
import com.project.cheerha.domain.keyword.repository.UserKeywordRepository;
import com.project.cheerha.domain.user.entity.User;
import com.project.cheerha.domain.user.repository.UserRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class UserKeywordService {
private final KeywordRepository keywordRepository;
private final UserKeywordRepository userKeywordRepository;
private final UserRepository userRepository;
@Transactional
public CreateUserKeywordResponseDto createUserKeyword(
AuthUser authUser,
CreateUserKeywordRequestDto requestDto
) {
Long userId = authUser.id();
User foundUser = findUserById(userId);
List<Long> idList = requestDto.keywordIdList();
List<Keyword> keywordList = findKeywordListByIdList(idList);
createNewUserKeywordIfNotExist(keywordList, foundUser);
List<String> keywordNameList = Keyword.extractNameListFromEntityList(keywordList);
return CreateUserKeywordResponseDto.of(keywordNameList);
}
// 등록된 UserKeyword가 없을 시 객체를 생성하고 저장하는 메서드
private void createNewUserKeywordIfNotExist(
List<Keyword> keywordList,
User foundUser
) {
keywordList.forEach(
foundKeyword -> {
boolean isKeywordAlreadyChosen = userKeywordRepository.existsByUserAndKeyword(
foundUser,
foundKeyword
);
if (!isKeywordAlreadyChosen) {
UserKeyword newUserKeyword = UserKeyword.of(
foundUser,
foundKeyword
);
userKeywordRepository.save(newUserKeyword);
}
}
);
}
// 키워드 식별자 목록으로 해당 키워드 엔티티를 조회하는 메서드
private List<Keyword> findKeywordListByIdList(List<Long> keywordIdList) {
return keywordIdList.stream()
.map(this::findKeywordById)
.toList();
}
// 키워드 식별자로 키워드를 조회하는 메서드
// 키워드가 존재하지 않을 시 에외 발생
// 목록 대신 식별자 하나로 조회할 수 있으므로 findKeywordListByIdList와 분리함
private Keyword findKeywordById(Long keywordId) {
return keywordRepository.findById(keywordId)
.orElseThrow(() -> new CustomException(ErrorCode.KEYWORD_NOT_FOUND));
}
// 사용자 식별자로 사용자를 조회하는 메서드
// 사용자가 없을 시 예외 발생
private User findUserById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
}
}
5번째 수정 후, 사용자가 1번부터 5번 키워드를 알림 받겠다고 이미 등록한 상태에서 Postman을 실행한 결과, 문제없이 원하는 값을 받을 수 있었다. 아래 사진은 기존에 등록한 키워드로만 요청을 보냈을 때 결과이다. ▼
그다음 사진은 기존에 등록한 5번 키워드와 아직 등록하지 않은 6번 키워드를 요청 값으로 보낸 결과이다. 6번 키워드는 데이터베이스에 저장되고 기존에 등록한 5번 키워드와 함께 응답 값에 포함되었다. ▼
---- 최종 ----
package com.project.cheerha.domain.keyword.entity;
import com.project.cheerha.domain.user.entity.User;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@NoArgsConstructor
@Table(name = "user_keyword", uniqueConstraints = {
@UniqueConstraint(columnNames = {"user_id", "keyword_id"})}
)
public class UserKeyword {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "keyword_id")
private Keyword keyword;
public static UserKeyword of(
User user,
Keyword keyword
) {
UserKeyword userKeyword = new UserKeyword();
userKeyword.user = user;
userKeyword.keyword = keyword;
return userKeyword;
}
}
package com.project.cheerha.domain.keyword.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@NoArgsConstructor
@Table(name = "keyword")
public class Keyword {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 50, nullable = false)
private String name;
// 키워드 객체 목록에서 이름을 추출하는 메서드
public static List<String> extractNameFromEntity(List<Keyword> keywordList) {
return keywordList.stream()
.map(Keyword::getName)
.toList();
}
}
package com.project.cheerha.domain.keyword.dto.request;
import java.util.List;
public record CreateUserKeywordRequestDto(List<Long> keywordIdList) {
}
package com.project.cheerha.domain.keyword.dto.response;
import java.util.List;
public record CreateUserKeywordResponseDto(List<String> keywordList) {
public static CreateUserKeywordResponseDto toDto(List<String> keywordList) {
return new CreateUserKeywordResponseDto(keywordList);
}
}
package com.project.cheerha.domain.keyword.repository;
import com.project.cheerha.domain.keyword.entity.UserKeyword;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserKeywordRepository extends JpaRepository<UserKeyword, Long> {
// userId와 keywordId로 UserKeyword가 존재하는지 확인하는 메서드
boolean existsByUserIdAndKeywordId(
Long userId,
Long keywordId
);
}
package com.project.cheerha.domain.keyword.service;
import com.project.cheerha.common.exception.CustomException;
import com.project.cheerha.common.exception.ErrorCode;
import com.project.cheerha.domain.keyword.dto.request.CreateUserKeywordRequestDto;
import com.project.cheerha.domain.keyword.dto.response.CreateUserKeywordResponseDto;
import com.project.cheerha.domain.keyword.entity.Keyword;
import com.project.cheerha.domain.keyword.entity.UserKeyword;
import com.project.cheerha.domain.keyword.repository.KeywordRepository;
import com.project.cheerha.domain.keyword.repository.UserKeywordRepository;
import com.project.cheerha.domain.user.entity.User;
import com.project.cheerha.domain.user.repository.UserRepository;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class UserKeywordService {
private final KeywordRepository keywordRepository;
private final UserKeywordRepository userKeywordRepository;
private final UserRepository userRepository;
@Transactional
public CreateUserKeywordResponseDto createUserKeyword(
Long userId,
CreateUserKeywordRequestDto requestDto
) {
List<Long> idList = requestDto.keywordIdList();
List<Keyword> keywordList = createNewUserKeywordIfNotExist(
userId,
idList
);
List<String> keywordNameList = Keyword.extractNameFromEntity(keywordList);
return CreateUserKeywordResponseDto.toDto(keywordNameList);
}
// UserKeyword 객체가 없을 시 객체를 생성하고 저장하는 메서드
private List<Keyword> createNewUserKeywordIfNotExist(
Long userId,
List<Long> keywordIdList
) {
List<Keyword> keywordList = new ArrayList<>();
keywordIdList.forEach(keywordId -> {
Keyword foundKeyword = keywordRepository.findById(keywordId)
.orElseThrow(
() -> new CustomException(ErrorCode.KEYWORD_NOT_FOUND)
);
if (!isKeywordAlreadyChosen(userId, keywordId)) {
User foundUser = userRepository.findById(userId)
.orElseThrow(
() -> new CustomException(ErrorCode.USER_NOT_FOUND)
);
UserKeyword newUserKeyword = UserKeyword.of(
foundUser,
foundKeyword
);
userKeywordRepository.save(newUserKeyword);
}
keywordList.add(foundKeyword);
}
);
return keywordList;
}
// 키워드가 이미 선택되었는지 확인하는 메서드
private boolean isKeywordAlreadyChosen(
Long userId,
Long keywordId
) {
return userKeywordRepository.existsByUserIdAndKeywordId(
userId,
keywordId
);
}
}
package com.project.cheerha.domain.keyword.controller;
import com.project.cheerha.common.annotation.Auth;
import com.project.cheerha.common.dto.AuthUser;
import com.project.cheerha.domain.keyword.dto.request.CreateUserKeywordRequestDto;
import com.project.cheerha.domain.keyword.dto.response.CreateUserKeywordResponseDto;
import com.project.cheerha.domain.keyword.service.UserKeywordService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/users/keywords")
@RestController
@RequiredArgsConstructor
public class UserKeywordController {
private final UserKeywordService userKeywordService;
@PostMapping
public ResponseEntity<CreateUserKeywordResponseDto> createUserKeywordList(
@RequestBody CreateUserKeywordRequestDto requestDto,
@Auth AuthUser authUser
) {
Long userId = authUser.id();
CreateUserKeywordResponseDto responseDto = userKeywordService.createUserKeyword(
userId,
requestDto
);
return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
}
}
-- (1) 외래 키 체크 비활성화
SET FOREIGN_KEY_CHECKS = 0;
-- (2) 'user_keyword' 테이블의 모든 데이터 삭제
TRUNCATE TABLE user_keyword;
-- (3) 'user_keyword' 테이블의 자동 증가 값을 1로 설정
ALTER TABLE user_keyword AUTO_INCREMENT = 1;
-- (4) 외래 키 체크 재활성화
SET FOREIGN_KEY_CHECKS = 1;