1515import org .springframework .http .HttpStatus ;
1616import org .springframework .http .HttpStatusCode ;
1717import org .springframework .stereotype .Service ;
18+ import org .springframework .transaction .PlatformTransactionManager ;
19+ import org .springframework .transaction .annotation .Propagation ;
1820import org .springframework .transaction .annotation .Transactional ;
21+ import org .springframework .transaction .support .TransactionTemplate ;
1922import org .springframework .util .StringUtils ;
2023import org .springframework .web .reactive .function .client .WebClient ;
2124import 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입니다. 테스트 검증을 위해 패키지 범위로 둡니다.
0 commit comments