Skip to content

Commit 948808e

Browse files
authored
Merge pull request #361 from CSID-DGU/develop
chore: develop → main 머지 (2026-07-13)
2 parents eb57dff + e7ab268 commit 948808e

5 files changed

Lines changed: 61 additions & 40 deletions

File tree

src/main/java/DGU_AI_LAB/admin_be/domain/groups/service/GroupService.java

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
import org.springframework.http.HttpStatus;
1616
import org.springframework.http.HttpStatusCode;
1717
import org.springframework.stereotype.Service;
18+
import org.springframework.transaction.PlatformTransactionManager;
19+
import org.springframework.transaction.annotation.Propagation;
1820
import org.springframework.transaction.annotation.Transactional;
21+
import org.springframework.transaction.support.TransactionTemplate;
1922
import org.springframework.util.StringUtils;
2023
import org.springframework.web.reactive.function.client.WebClient;
2124
import org.springframework.web.reactive.function.client.WebClientResponseException;
@@ -34,6 +37,7 @@ public class GroupService {
3437
private final GroupRepository groupRepository;
3538
private final RequestRepository requestRepository;
3639
private final @Qualifier("configWebClient") WebClient groupCreationWebClient;
40+
private final PlatformTransactionManager transactionManager;
3741

3842
/**
3943
* 모든 그룹 정보를 조회하는 API
@@ -56,34 +60,32 @@ public List<GroupResponseDTO> getAllGroups() {
5660
* 새로운 그룹을 생성하는 API
5761
* POST /api/groups
5862
*/
59-
@Transactional
63+
@Transactional(propagation = Propagation.NOT_SUPPORTED)
6064
public GroupResponseDTO createGroup(CreateGroupRequestDTO dto, Long userId) {
6165

6266
log.info("[createGroup] 그룹 생성 요청 시작: groupName={}, ubuntuUsername={}", dto.groupName(), dto.ubuntuUsername());
6367

64-
// 1. ubuntuUsername이 제공된 경우에만 유효성 검사 (필수 X)
65-
if (StringUtils.hasText(dto.ubuntuUsername())) {
66-
if (!requestRepository.existsByUbuntuUsernameAndUser_UserId(dto.ubuntuUsername(), userId)) {
67-
throw new BusinessException(ErrorCode.FORBIDDEN_REQUEST);
68+
// 1. 트랜잭션 안에서 DB 유효성 검사
69+
TransactionTemplate txTemplate = new TransactionTemplate(transactionManager);
70+
txTemplate.executeWithoutResult(status -> {
71+
if (StringUtils.hasText(dto.ubuntuUsername())) {
72+
if (!requestRepository.existsByUbuntuUsernameAndUser_UserId(dto.ubuntuUsername(), userId)) {
73+
throw new BusinessException(ErrorCode.FORBIDDEN_REQUEST);
74+
}
6875
}
69-
}
70-
71-
// 2. DB에서 그룹명 중복을 먼저 확인합니다.
72-
if (groupRepository.existsByGroupName(dto.groupName())) {
73-
log.warn("[createGroup] DB에 이미 존재하는 그룹명입니다: {}", dto.groupName());
74-
throw new BusinessException(ErrorCode.DUPLICATE_GROUP_NAME);
75-
}
76+
if (groupRepository.existsByGroupName(dto.groupName())) {
77+
log.warn("[createGroup] DB에 이미 존재하는 그룹명입니다: {}", dto.groupName());
78+
throw new BusinessException(ErrorCode.DUPLICATE_GROUP_NAME);
79+
}
80+
});
7681

77-
// 3. 외부 API 호출을 위한 ubuntuUser 멤버 리스트를 구성합니다.
82+
// 2. 트랜잭션 종료 후 외부 API 호출 (커넥션 미점유)
7883
List<String> members = Optional.ofNullable(dto.ubuntuUsername())
7984
.filter(StringUtils::hasText)
8085
.map(List::of)
8186
.orElse(Collections.emptyList());
8287

83-
ConfigServerGroupRequest apiDto = new ConfigServerGroupRequest(
84-
dto.groupName(),
85-
members
86-
);
88+
ConfigServerGroupRequest apiDto = new ConfigServerGroupRequest(dto.groupName(), members);
8789

8890
ConfigServerGroupResponse apiResponse;
8991
try {
@@ -143,26 +145,27 @@ public GroupResponseDTO createGroup(CreateGroupRequestDTO dto, Long userId) {
143145
throw new BusinessException(ErrorCode.GID_ALLOCATION_FAILED);
144146
}
145147

146-
if (groupRepository.existsByUbuntuGid(assignedGid)) {
147-
log.warn("[createGroup] DB에 이미 존재하는 GID입니다: {}", assignedGid);
148-
throw new BusinessException(ErrorCode.DUPLICATE_GROUP_ID);
149-
}
150-
151-
// 4. API 호출이 성공한 후에만 로컬 DB에 그룹을 저장합니다.
152-
Group group = Group.builder()
153-
.groupName(dto.groupName())
154-
.ubuntuGid(assignedGid)
155-
.build();
156-
157-
try {
158-
group = groupRepository.save(group);
159-
} catch (Exception e) {
160-
log.error("[createGroup] 인프라에 그룹 생성됨, DB 저장 실패 — 수동 정리 필요: groupName={}, gid={}", dto.groupName(), assignedGid, e);
161-
throw new BusinessException(ErrorCode.GROUP_CREATION_FAILED);
162-
}
163-
log.info("[createGroup] 그룹 생성 및 로컬 DB 저장 완료: id={}, name={}", group.getGroupId(), group.getGroupName());
148+
// 3. API 성공 후 새 트랜잭션에서 DB 저장
149+
final Long finalGid = assignedGid;
150+
Group savedGroup = txTemplate.execute(status -> {
151+
if (groupRepository.existsByUbuntuGid(finalGid)) {
152+
log.warn("[createGroup] DB에 이미 존재하는 GID입니다: {}", finalGid);
153+
throw new BusinessException(ErrorCode.DUPLICATE_GROUP_ID);
154+
}
155+
Group group = Group.builder()
156+
.groupName(dto.groupName())
157+
.ubuntuGid(finalGid)
158+
.build();
159+
try {
160+
return groupRepository.save(group);
161+
} catch (Exception e) {
162+
log.error("[createGroup] 인프라에 그룹 생성됨, DB 저장 실패 — 수동 정리 필요: groupName={}, gid={}", dto.groupName(), finalGid, e);
163+
throw new BusinessException(ErrorCode.GROUP_CREATION_FAILED);
164+
}
165+
});
164166

165-
return GroupResponseDTO.fromEntity(group);
167+
log.info("[createGroup] 그룹 생성 및 로컬 DB 저장 완료: id={}, name={}", savedGroup.getGroupId(), savedGroup.getGroupName());
168+
return GroupResponseDTO.fromEntity(savedGroup);
166169
}
167170

168171
// GroupService에서만 사용하는 infra API 요청/응답 DTO입니다. 테스트 검증을 위해 패키지 범위로 둡니다.

src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/SaveRequestRequestDTO.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,12 @@ public Request toEntity(
8080
}
8181

8282
// 클라이언트는 base64 인코딩된 값을 전송 — 평문은 디코딩해서 이메일 발송용으로 따로 보관
83-
String plainPassword = new String(Base64.getDecoder().decode(ubuntuPasswordBase64), StandardCharsets.UTF_8);
83+
String plainPassword;
84+
try {
85+
plainPassword = new String(Base64.getDecoder().decode(ubuntuPasswordBase64), StandardCharsets.UTF_8);
86+
} catch (IllegalArgumentException e) {
87+
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
88+
}
8489

8590
Request req = Request.builder()
8691
.user(user)

src/main/java/DGU_AI_LAB/admin_be/domain/requests/repository/RequestRepository.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ public interface RequestRepository extends JpaRepository<Request, Long> {
2929
@Query("SELECT r.ubuntuUsername FROM Request r WHERE r.status = :status")
3030
List<String> findUbuntuUsernamesByStatus(@Param("status") Status status);
3131

32+
@Query("SELECT DISTINCT r FROM Request r " +
33+
"JOIN FETCH r.user " +
34+
"LEFT JOIN FETCH r.resourceGroup " +
35+
"LEFT JOIN FETCH r.containerImage " +
36+
"LEFT JOIN FETCH r.requestGroups rg " +
37+
"LEFT JOIN FETCH rg.group " +
38+
"WHERE r.status = :status")
39+
List<Request> findAllByStatusWithAssociations(@Param("status") Status status);
40+
3241
@Query("SELECT r FROM Request r JOIN FETCH r.user JOIN FETCH r.resourceGroup WHERE r.expiresAt BETWEEN :start AND :end AND r.status = :status")
3342
List<Request> findAllByExpiresAtBetweenAndStatus(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end, @Param("status") Status status);
3443

src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/AdminRequestCommandService.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,11 @@ public SaveRequestResponseDTO approveRequest(ApproveRequestDTO dto) {
138138
.usagePurpose(port.usagePurpose())
139139
.build());
140140
}
141-
req.getUser().getEmail(); // lazy 연관 초기화 (트랜잭션 종료 후 이메일 발송 시 필요)
141+
// 트랜잭션 종료 후 사용되는 모든 lazy 연관 초기화
142+
req.getUser().getEmail();
143+
req.getContainerImage().getImageName();
144+
req.getResourceGroup().getServerName();
145+
req.getRequestGroups().size();
142146
savedRequestRef[0] = req;
143147
return null;
144148
});

src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/AdminRequestQueryService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ public List<SaveRequestResponseDTO> getNewRequests() {
3535
}
3636

3737
public List<ResourceUsageDTO> getAllFulfilledResourceUsage() {
38-
return requestRepository.findAllByStatus(Status.FULFILLED).stream()
38+
return requestRepository.findAllByStatusWithAssociations(Status.FULFILLED).stream()
3939
.map(ResourceUsageDTO::fromEntity)
4040
.toList();
4141
}
4242

4343
public List<ContainerInfoDTO> getAllActiveContainers() {
44-
return requestRepository.findAllByStatus(Status.FULFILLED).stream()
44+
return requestRepository.findAllByStatusWithAssociations(Status.FULFILLED).stream()
4545
.map(ContainerInfoDTO::fromEntity)
4646
.toList();
4747
}

0 commit comments

Comments
 (0)