
Facade Pattern (ํผ์ฌ๋ ํจํด)
ํผ์ฌ๋ ํจํด(Facade Pattern)์ ๊ตฌ์กฐ ํจํด(Structural Pattern)์ ํ ์ข
๋ฅ๋ก, ๋ณต์กํ ์๋ธ ์์คํ
์ ๊ธฐ๋ฅ๋ค์ ๊ฐ๋จํ ์์ ์์ค์ ์ธํฐํ์ด์ค๋ก ์ ๊ณตํ์ฌ ํด๋ผ์ด์ธํธ์ ์๋ธ ์์คํ
๊ฐ์ ์์กด์ฑ์ ์ค์ด๋ ์ญํ ์ ํฉ๋๋ค.
์ด๋ฅผ ํตํด ํด๋ผ์ด์ธํธ๋ ๋ด๋ถ์ ๋ณต์กํ ๋ก์ง์ ์ ๊ฒฝ ์ฐ์ง ์๊ณ ๋, ๋จ์ํ๋ ์ ๊ทผ ๋ฐฉ์์ผ๋ก ํ์ํ ์์
์ ์ํํ ์ ์์ต๋๋ค.
๋ฎ์ ๊ฒฐํฉ๋
ํด๋ผ์ด์ธํธ๋ ํผ์ฌ๋ ๊ฐ์ฒด๋ง ์๋ฉด ๋๋ฏ๋ก ์๋ธ ์์คํ
์ ๋ด๋ถ ๊ตฌํ์ ์์กดํ์ง ์์ต๋๋ค.
๊ฐ๋
์ฑ ์ฆ๊ฐ
ํด๋ผ์ด์ธํธ์์ ์ฌ๋ฌ ์๋ธ ์์คํ
์ ํธ์ถํ๋ ๋์ , ๋จ์ผ ํผ์ฌ๋ ๊ฐ์ฒด๋ก ์์
์ ์ฒ๋ฆฌํ์ฌ ์ฝ๋์ ๊ฐ๋
์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ๋์
๋๋ค.
์๋ธ ์์คํ
์ง์ ์ ๊ทผ ๊ฐ๋ฅ
ํ์์ ๋ฐ๋ผ ์๋ธ ์์คํ
๊ฐ์ฒด๋ฅผ ์ง์ ์ฌ์ฉํ ์๋ ์์ต๋๋ค. ์ ํ์ง๊ฐ ์ ์ฐํด์ง๋๋ค.
์ฐธ๊ณ : ์ ๋ฏธ๋์ ๊ฐ๋ฐ์ค๋ฌด
๊ธฐ์กด ๋ฐฉ์์์๋ ํด๋ผ์ด์ธํธ๊ฐ "๋์ ํ์ ์ ๋ณด ์กฐํ"์ "๋ค๋ฅธ ํ์ ์ ๋ณด ์กฐํ"๋ฅผ ์์ฒญํ ๋, ์๋น์ค ๋ ์ด์ด์์ ๋ณ๋๋ก DTO๋ฅผ ๊ตฌ์ฑํ์ฌ ๋ฐํํ์ต๋๋ค.
์ด๋ ๋น์ทํ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ์ง๋ง, ์๋น์ค ๋ก์ง์ด ์ค๋ณต๋๊ณ ํ์ฅ์ฑ์ด ๋จ์ด์ง๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.

// UserController์ ๊ธฐ์กด ๋ฐฉ์
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
// ๋์ ํ์์ ๋ณด ์กฐํ
@GetMapping
public ResponseEntity<?> getUserInfo(Authentication authentication) {
return ResponseEntity.status(OK)
.body(ResponseMessage.success(userService.getUserInfo(getCurrentUser(authentication))));
}
// ๋ค๋ฅธ ํ์ ์ ๋ณด ์กฐํ
@GetMapping("/{userId}")
public ResponseEntity<?> getOtherUserInfo(@PathVariable Long userId) {
return ResponseEntity.status(OK)
.body(ResponseMessage.success(userService.getOtherUserInfo(userId)));
}
private UserEntity getCurrentUser(Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
throw new BizException(UNAUTHORIZED_ERROR);
}
return userRepository.findByEmail(((CustomUserDetails) authentication.getPrincipal()).getUsername())
.orElseThrow(() -> new BizException(USER_NOT_FOUND_ERROR));
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
// ๋์ ํ์์ ๋ณด ์กฐํ ๋ก์ง
public UserInfoResponseDTO getUserInfo(UserEntity currentUser) {
return UserInfoResponseDTO.builder()
.id(currentUser.getId())
.username(currentUser.getUsername())
.nickname(currentUser.getNickname())
.phone(currentUser.getPhone())
.email(currentUser.getEmail())
.status(currentUser.getStatus())
.build();
}
// ๋ค๋ฅธ ํ์ ์ ๋ณด ์กฐํ ๋ก์ง
public OtherUserInfoResponseDTO getOtherUserInfo(Long userId) {
UserEntity userEntity = userRepository.findById(userId)
.orElseThrow(()->new BizException(USER_NOT_FOUND_ERROR));
return OtherUserInfoResponseDTO.builder()
.username(userEntity.getUsername())
.nickname(userEntity.getNickname())
.email(userEntity.getEmail())
.status(userEntity.getStatus())
.build();
}
}
์ค๋ณต๋ ๋ก์ง
UserService์์ "๋์ ํ์ ์ ๋ณด ์กฐํ"์ "๋ค๋ฅธ ํ์ ์ ๋ณด ์กฐํ" ๋ก์ง์ด ๊ฑฐ์ ๋์ผํ์ง๋ง, ๋ณ๋์ DTO๋ฅผ ์ฌ์ฉํ์ฌ ๋ ๋ฒ ์์ฑ๋จ.ํ์ฅ์ฑ ๋ฌธ์
์ปจํธ๋กค๋ฌ์ ๋ณต์ก์ฑ
๊ณตํต ๋ฐ์ดํฐ๋ ์๋น์ค ๋ ์ด์ด์์ ์ฒ๋ฆฌํ๊ณ ,
Facade ๋ ์ด์ด์์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์กฐํฉํ์ฌ ํด๋ผ์ด์ธํธ ์์ฒญ์ ๋ง๊ฒ ๋ฐํํฉ๋๋ค.
์ด๋ฅผ ํตํด ์๋น์ค ๋ก์ง์ ์ค๋ณต ์ ๊ฑฐ์ ํ์ฅ์ฑ ํฅ์์ ๋์์ ๋ฌ์ฑํ ์ ์์ต๋๋ค.
@Service
@Slf4j
@RequiredArgsConstructor
public class UserInfoFacade {
private final UserParticipationService userParticipationService;
private final UserService userService;
private final RedisTemplate<String, Object> redisTemplate;
public UserInfoFacadeDto getUserInfo(Long userId) {
String cacheKey = "userInfo:" + userId;
// Redis์์ ์บ์๋ ๋ฐ์ดํฐ ์กฐํ
Object cachedData = redisTemplate.opsForValue().get(cacheKey);
if (cachedData != null) {
log.info("Cache hit for user ID: {}", userId);
return (UserInfoFacadeDto) cachedData;
}
UserServiceDto userInfo = userService.getUserInfo(userId);
Integer completedTripsCount = userParticipationService.getCompletedTripsCount(
String.valueOf(userId));
UserInfoFacadeDto build = UserInfoFacadeDto.builder()
.id(userInfo.getId())
.count(completedTripsCount)
.username(userInfo.getUsername())
.phone(userInfo.getPhone())
.email(userInfo.getEmail())
.mbti(userInfo.getMbti())
.ratingAvg(userInfo.getRatingAvg())
.birth(userInfo.getBirth())
.introduction(userInfo.getIntroduction())
.nickname(userInfo.getNickname())
.status(userInfo.getStatus())
.smoking(userInfo.getSmoking())
.gender(userInfo.getGender())
.build();
// Redis์ ์บ์ ์ ์ฅ (๋ง๋ฃ ์๊ฐ 1์๊ฐ ์ค์ )
redisTemplate.opsForValue().set(cacheKey, build, 1, TimeUnit.HOURS);
log.info("Cache miss for user ID: {}. Data cached.", userId);
return build;
}
}
public UserServiceDto getUserInfo(long userId) {
UserEntity userEntity = userRepository.findById(userId).orElseThrow(()->new BizException(USER_NOT_FOUND_ERROR));
ProfileEntity profileEntity = profileRepository.findByUserId(userEntity.getId())
.orElseThrow(() -> new BizException(PROFILE_NOT_FOUND_ERROR));
// ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํ
UserServiceDto userInfo = UserServiceDto.builder()
.id(userEntity.getId())
.username(userEntity.getUsername())
.nickname(userEntity.getNickname())
.phone(userEntity.getPhone())
.email(userEntity.getEmail())
.status(userEntity.getStatus())
.introduction(profileEntity.getIntroduction())
.mbti(profileEntity.getMbti())
.gender(profileEntity.getGender())
.smoking(profileEntity.getSmoking())
.birth(profileEntity.getBirth())
.ratingAvg(profileEntity.getRatingAvg())
.build();
return userInfo;
}
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/users")
public class UserController {
private final UserInfoFacade userInfoFacade;
// ๋์ ํ์์ ๋ณด ์กฐํ
@GetMapping
public ResponseEntity<ResponseMessage<UserInfoResponseDTO>> getUserInfo(
Authentication authentication) {
UserInfoFacadeDto otherUserInfo = userInfoFacade.getUserInfo(getCurrentUser(authentication).getId());
return ResponseEntity.status(OK)
.body(ResponseMessage.success(UserInfoResponseDTO.fromDto(otherUserInfo)));
}
// ๋ค๋ฅธ ํ์ ์ ๋ณด ์กฐํ
@GetMapping("/{userId}")
public ResponseEntity<ResponseMessage<OtherUserInfoResponseDTO>> getOtherUserInfo(
@PathVariable Long userId) {
UserInfoFacadeDto otherUserInfo = userInfoFacade.getUserInfo(userId);
return ResponseEntity.status(OK)
.body(ResponseMessage.success(OtherUserInfoResponseDTO.fromDto(otherUserInfo)));
}
@Data
@Builder
@Getter
@AllArgsConstructor
@JsonDeserialize(builder = UserInfoResponseDTO.UserInfoResponseDTOBuilder.class)
public class UserInfoResponseDTO {
private Long id;
private String username;
private String nickname;
private String phone;
private String email;
private String status;
private MBTI mbti;
private String smoking;
private String introduction;
private String gender;
@JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateDeserializer.class)
private LocalDate birth;
private Double ratingAvg;
public static UserInfoResponseDTO fromDto(UserInfoFacadeDto userInfoFacadeDto) {
return UserInfoResponseDTO.builder()
.id(userInfoFacadeDto.getId())
.username(userInfoFacadeDto.getUsername())
.nickname(userInfoFacadeDto.getNickname())
.phone(userInfoFacadeDto.getPhone())
.email(userInfoFacadeDto.getEmail())
.mbti(userInfoFacadeDto.getMbti())
.introduction(userInfoFacadeDto.getIntroduction())
.birth(userInfoFacadeDto.getBirth())
.ratingAvg(userInfoFacadeDto.getRatingAvg())
.smoking(userInfoFacadeDto.getSmoking().getSmoke())
.gender(userInfoFacadeDto.getGender().getGender())
.status(userInfoFacadeDto.getStatus().getStatus())
.build();
}
@JsonPOJOBuilder(withPrefix = "")
public static class UserInfoResponseDTOBuilder {
}
}
@Builder
@Getter
@AllArgsConstructor
@JsonDeserialize(builder = OtherUserInfoResponseDTO.OtherUserInfoResponseDTOBuilder.class)
public class OtherUserInfoResponseDTO {
private String username;
private String nickname;
private String email;
private Integer count; // ์ฌํ ํ์
private Double ratingAvg;
private String gender;
private String status;
public static OtherUserInfoResponseDTO fromDto(UserInfoFacadeDto userInfoFacadeDto) {
return OtherUserInfoResponseDTO.builder()
.username(userInfoFacadeDto.getUsername())
.nickname(userInfoFacadeDto.getNickname())
.email(userInfoFacadeDto.getEmail())
.count(userInfoFacadeDto.getCount())
.ratingAvg(userInfoFacadeDto.getRatingAvg())
.gender(userInfoFacadeDto.getGender().getGender())
.status(userInfoFacadeDto.getStatus().getUserStatus())
.build();
}
@JsonPOJOBuilder(withPrefix = "")
public static class OtherUserInfoResponseDTOBuilder {
}
}
@Service
@RequiredArgsConstructor
public class UserParticipationService {
private final TravelClient travelClient;
public Integer getCompletedTripsCount(String userId) {
ResponseEntity<Integer> response = travelClient.getParticipationsCompletedByUserId(userId);
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody();
} else {
throw new BizException(INTERNAL_SERVER_ERROR);
}
}
}
์ฌ์ฌ์ฉ์ฑ ์ฆ๊ฐ
์ค๋ณต ์ฝ๋ ์ ๊ฑฐ
ํ์ฅ์ฑ ๋ฐ ์ ์ง๋ณด์์ฑ ํฅ์
ํผ์ฌ๋ ํจํด์ ์ ์ฉํ์ฌ, ๊ธฐ์กด์ ์ค๋ณต๋ ๋ก์ง์ ์ ๊ฑฐํ๊ณ ์ ์ฐํ ๋ฐ์ดํฐ ๋ฐํ ๊ตฌ์กฐ๋ฅผ ์ค๊ณํ์ต๋๋ค.
ํด๋ผ์ด์ธํธ๋ ๋ด๋ถ ๋ก์ง์ ์ ๊ฒฝ ์ฐ์ง ์๊ณ Facade ๊ฐ์ฒด๋ฅผ ํตํด ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ฒ ํธ์ถํ ์ ์์ผ๋ฉฐ, ์ด๋ ๊ฐ๋
์ฑ๊ณผ ํ์ฅ์ฑ ๋ชจ๋๋ฅผ ๋์ด๋ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ธ์์ต๋๋ค.

Facade์ ์ญํ ๋ช
ํํ:
ํ์ฌ UserInfoFacade๊ฐ ๋จ์ํ ๋ฐ์ดํฐ๋ฅผ ์กฐํฉํ๊ณ ์บ์๋ฅผ ๊ด๋ฆฌํ๋ ์ญํ ์ ํ๊ณ ์์ต๋๋ค. Facade ํจํด์ ๋ชฉ์ ์ ํด๋ผ์ด์ธํธ์๊ฒ ์๋ธ ์์คํ
์ ๋ณต์ก์ฑ์ ์จ๊ธฐ๋ ๊ฒ์ด๋ฏ๋ก, ์ถ๊ฐ์ ์ธ ์๋ธ ์์คํ
์ด๋ ๋ณต์กํ ๋ก์ง์ด ๋ ์๋ค๋ฉด Facade๊ฐ ์ด๋ฅผ ๋ ์ ๊ฐ์ธ๋๋ก ํ ์ ์์ต๋๋ค.
์๋ฅผ ๋ค์ด, ์ธ์ฆ, ๊ถํ ๊ฒ์ฌ, ๋ก๊น
๋ฑ ๋ค๋ฅธ ์๋ธ ์์คํ
๊ณผ์ ์ฐ๊ณ๊ฐ ์๋ค๋ฉด Facade์์ ์ด๋ฅผ ํตํฉ ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
DTO ๋ณํ ๋ก์ง์ ์์น:
ํ์ฌ DTO ๋ณํ ๋ก์ง์ด Facade์ ์ปจํธ๋กค๋ฌ ์์ชฝ์ ๋ถ์ฐ๋์ด ์์ต๋๋ค. ๋ณํ ๋ก์ง์ ์ผ๊ด๋๊ฒ ๊ด๋ฆฌํ๊ธฐ ์ํด Facade ๋ด์์ ๋ชจ๋ DTO ๋ณํ์ ์ฒ๋ฆฌํ๊ณ , ์ปจํธ๋กค๋ฌ๋ Facade์์ ๋ฐํ๋ DTO๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ๋ ๋ฐฉ์์ผ๋ก ํต์ผํ ์ ์์ต๋๋ค.
์บ์ ๊ด๋ฆฌ์ ์ถ์ํ:
์บ์ ๋ก์ง์ด Facade์ ์ง์ ํฌํจ๋์ด ์๋๋ฐ, ์ด๋ฅผ ๋ณ๋์ ์บ์ ๊ด๋ฆฌ ์๋น์ค๋ก ๋ถ๋ฆฌํ๋ฉด Facade์ ์ฑ ์์ ๋์ฑ ๋ช ํํ ํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, UserInfoCacheService๋ฅผ ๋ง๋ค์ด ์บ์ ๊ด๋ จ ๋ก์ง์ ์ ๋ดํ๊ฒ ํ ์ ์์ต๋๋ค.