#service, ย #test

์„œ๋น„์Šค ์ถ”์ƒํ™”

์„œ๋น„์Šค ์ถ”์ƒํ™”

๐Ÿ˜˜ ์„œ๋ก 

์šฐ์•„ํ•œํ…Œํฌ์ฝ”์Šค์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํ˜‘์—… ๋„๊ตฌ ์ค‘ ํ•˜๋‚˜์ธ Slack์€ ๋ฌด๋ฃŒ ํ”„๋ฆฌํ‹ฐ์–ด ์‚ฌ์šฉ ์‹œ 3๊ฐœ์›”์ด ์ง€๋‚œ ๋ฉ”์‹œ์ง€๋“ค์„ ๋ณด์—ฌ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ ˆ๋ฒจ 3 ํŒ€ ํ”„๋กœ์ ํŠธ์ธ ์ค์ค์—์„œ๋Š” ์ด ์‚ฌ๋ผ์ง€๋Š” ๋ฉ”์‹œ์ง€๋“ค์„ ๋ฐฑ์—…ํ•ด์ฃผ๋Š” ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Slack์˜ ๋ฉ”์‹œ์ง€์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์ฃผ๋กœ ๋‹ค๋ฃจ๋‹ค ๋ณด๋‹ˆ Slack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์˜์กด์ ์ธ ๋กœ์ง์ด ๋งŽ์Šต๋‹ˆ๋‹ค. ์ด ํฌ์ŠคํŒ…์—์„œ๋Š” ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(Slack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋กœ์ง์„ ์–ด๋–ป๊ฒŒ, ์™œ ์ถ”์ƒํ™”ํ–ˆ๋Š”์ง€์— ๊ด€ํ•ด ์ด์•ผ๊ธฐํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.



๐Ÿค” ์™œ ์„œ๋น„์Šค๋ฅผ ์ถ”์ƒํ™”ํ•ด์•ผ ํ•˜๋Š”๊ฐ€?

2022 10 10 slack

๊ธฐ์กด์˜ ํ…Œ์ŠคํŠธ์—์„œ๋Š” Slack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ถ€๋ถ„์€ ๋ชจํ‚น ์ฒ˜๋ฆฌํ•ด๋‘์—ˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋ฆฌํŒฉํ† ๋งํ•˜๋‹ค ๋ณด๋‹ˆ ๋ชจํ‚น ์ž‘์—…์ด ๊ณ„์† ๋ฐ˜๋ณต๋˜๋Š” ์ง€๋ฃจํ•œ ์ž‘์—…์œผ๋กœ ๋Š๊ปด์กŒ์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋ณต๋˜๋Š” ๋ชจํ‚น ์ž‘์—…์„ ์ œ๊ฑฐํ•˜๊ธฐ ์œ„ํ•ด ํ…Œ์ŠคํŠธ ๋ฐฉ์‹์„ ๋ชฉ์—์„œ ์Šคํ…์œผ๋กœ ๋ณ€๊ฒฝํ•ด์•ผ๊ฒ ๋‹ค๋Š” ๊ฒฐ์‹ฌ์„ ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•œ ์ด์•ผ๊ธฐ๋Š” 2๊ธฐ_์Šคํ‹ฐ์น˜์˜ Test Double์„ ์•Œ์•„๋ณด์ž๋ฅผ ์ฐธ๊ณ  ๋ฐ”๋ž๋‹ˆ๋‹ค.)

Slack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์Šคํ…์šฉ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” MethodsClient๋ผ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ์ธํ„ฐํŽ˜์ด์Šค๋Š” ์•ฝ 250๊ฐœ์˜ ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ–๊ณ  ์žˆ์œผ๋ฉฐ ๊ทธ์ค‘ ์ €ํฌ ์„œ๋น„์Šค์—์„œ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ๋Š” ์•ฝ 10๊ฐœ ์ •๋„์— ๋ถˆ๊ณผํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ๋ชจ๋‘ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์€ ๋ถˆํ•„์š”ํ•˜๊ณ  ๋ฒˆ๊ฑฐ๋กœ์šด ์ž‘์—…์ด๊ธฐ์— ๋” ๊ฐ„ํŽธํ•œ ๋ฐฉ๋ฒ•์„ ๊ณ ๋ฏผํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋•Œ ๋– ์˜ค๋ฅธ ๋ฐฉ๋ฒ•์ด ์„œ๋น„์Šค๋ฅผ ์ถ”์ƒํ™”ํ•ด ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ์‚ฌ์šฉํ•˜๋„๋ก ๋งŒ๋“œ๋Š” ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ์ž์„ธํ•œ ์„ค๋ช…์€ ์•„๋ž˜ ์˜ˆ์ œ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉฐ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.



๐Ÿ˜† ์˜ˆ์ œ ์ฝ”๋“œ

0. ๊ธฐ์กด ์ฝ”๋“œ

์˜ˆ์ œ๋กœ ์ฑ„๋„์„ ์ƒ์„ฑํ•˜๋Š” ChannelCreateService์˜ ๊ธฐ์กด ๋กœ์ง์„ ๊ฐ€์ ธ์™€ ๋ณด์•˜์Šต๋‹ˆ๋‹ค. ๋กœ์ง ๋Œ€๋ถ€๋ถ„์ด Slack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์˜์กดํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


@Transactional
@Service
public class ChannelCreatedService {

  private MethodsClient slackClient;
  private ChannelRepository channels;

  public ChannelCreatedService(final MethodsClient slackClient, final ChannelRepository channels) {
    this.slackClient = slackClient;
    this.channels = channels;
  }

  public Channel execute(final String channelSlackId) {
    try {
      // slack api๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ถ€๋ถ„
      Conversation conversation = slackClient
        .conversationsInfo(request -> request.channel(channelSlackId))
        .getChannel();

      // slack์˜ response๋ฅผ ์šฐ๋ฆฌ ๋„๋ฉ”์ธ์ธ channel ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜
      Channel channel = toChannel(conversation);

      // channel์„ ์ €์žฅํ•˜๊ณ  return
      return channels.save(channel);

    } catch (IOException | SlackApiException e) { // slack api ํ˜ธ์ถœ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” exception
      throw new SlackApiCallException();
    }
  }
}

2022 10 10 service ver1


1. ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๋Š” ๋กœ์ง ์ถ”์ƒํ™”

์œ„ ๋กœ์ง์„ ์•„๋ž˜์ฒ˜๋Ÿผ ๋‘ ๊ฐœ์˜ ํด๋ž˜์Šค๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์—ญํ• ์„ ๋‚˜๋ˆ ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

ChannelCreateService

  • ์ฑ„๋„ ์ €์žฅ ๋กœ์ง์„ ๊ฐ€์ง„ ํด๋ž˜์Šค

@Transactional
@Service
public class ChannelCreatedService {

  private CallSlackApi slackClient;
  private ChannelRepository channels;

  public ChannelCreatedService(final CallSlackApi slackClient, final ChannelRepository channels) {
    this.slackClient = slackClient;
    this.channels = channels;
  }

  public Channel execute(final String channelSlackId) {
    Channel channel = slackClient.callChannel(channelSlackId);
    return channels.save(channel);
  }
}

CallSlackApi

  • Slack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ๊ฒฐ๊ด๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ํด๋ž˜์Šค

@Component
public class CallSlackApi {

  private final MethodsClient methodsClient;

  public CallSlackApi(final MethodsClient methodsClient) {
    this.methodsClient = methodsClient;
  }

  public Channel callChannel(final String channelSlackId) {
    try {
      // `Slack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ`๋ฅผ ํ˜ธ์ถœ
      Conversation conversation = methodsClient.conversationsInfo(
          request -> request.channel(channelSlackId))
        .getChannel();

      // ๊ฒฐ๊ณผ๊ฐ’์„ ์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ์ฒด ํ˜•ํƒœ๋กœ ๋ณ€๊ฒฝ
      return new Channel(conversation.getId(), conversation.getName());

    } catch (IOException | SlackApiException e) {
      throw new SlackApiCallException();
    }
  }
}

2022 10 10 service ver2


2. ์ถ”์ƒํ™”ํ•œ ๋กœ์ง ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋ถ„๋ฆฌ

์ข€ ๋” ํŽธ๋ฆฌํ•œ ์Šคํ… ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ExternalClient๋ผ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด์˜ CallSlackApi์— ์กด์žฌํ•˜๋˜ ๋กœ์ง์€ ExternalClient ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ตฌํ˜„์ฒด์ธ SlackClient๋กœ ์˜ฎ๊ฒผ์Šต๋‹ˆ๋‹ค.

ExternalClient

  • ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๋กœ์ง์„ ์ถ”์ƒํ™”ํ•œ ์ธํ„ฐํŽ˜์ด์Šค
public interface ExternalClient {
  Channel callChannel(String channelSlackId);
}

SlackClient

  • Slack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ExternalClient์˜ ๊ตฌํ˜„์ฒด

@Component
public class SlackClient implements ExternalClient {

  private MethodsClient methodsClient;

  public SlackClient(final MethodsClient methodsClient) {
    this.methodsClient = slackClient;
  }

  public Channel callChannel(final String channelSlackId) {
    try {
      Conversation conversation = methodsClient.conversationsInfo(
          request -> request.channel(channelSlackId))
        .getChannel();

      return new Channel(conversation.getId(), conversation.getName());

    } catch (IOException | SlackApiException e) {
      throw new SlackApiCallException();
    }
  }
}

2022 10 10 service ver3


3. ์Šคํ…์šฉ ๊ฐ์ฒด ์ƒ์„ฑ ๋ฐ ์ ์šฉ

์Šคํ…์šฉ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ ๋’ค @Component๋ฅผ ์ด์šฉํ•ด bean์œผ๋กœ ๋“ฑ๋กํ•˜์˜€์Šต๋‹ˆ๋‹ค.


@Primary // SlackClient ๋ณด๋‹ค ์šฐ์„  ์ˆœ์œ„๊ฐ€ ๋†’์€ bean์œผ๋กœ ๋“ฑ๋กํ•˜๊ธฐ ์œ„ํ•ด ๋ถ™์˜€๋‹ค.
@Component
public class FakeClient implements ExternalClient {

  private List<Channel> channels = List.of( ...); // ์ฑ„๋„ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”

  @Override
  public Channel callChannel(final String channelSlackId) {
    return channels.stream()
      .filter(it -> it.isSameSlackId(channelSlackId))
      .findAny()
      .orElseThrow(() -> new SlackApiCallException());
  }
}

4. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ๋ชจํ‚น ์ œ๊ฑฐ

์Šคํ…์„ ์ ์šฉํ•˜์˜€์œผ๋‹ˆ ์ด์ œ ๋ชจํ‚นํ•œ ์ฝ”๋“œ๋“ค์„ ์ œ๊ฑฐํ•ด์ฃผ๋Š” ์ผ๋งŒ ๋‚จ์•˜์Šต๋‹ˆ๋‹ค. ์ „ํ›„ ์ฝ”๋“œ๋ฅผ ๋น„๊ตํ•˜์—ฌ ์ฝ”๋“œ๊ฐ€ ์–ผ๋งˆ๋‚˜ ๊น”๋”ํ•ด์กŒ๋Š”์ง€ ํ™•์ธํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

AS-IS (๋ชฉ ์ ์šฉ)


@Test
void execute() {
  // given
  Workspace workspace = workspaces.save(WorkspaceFixture.JUPJUP.create());
  Channel channel = ChannelFixture.QNA.create(workspace);
  String request = createRequest(channel);

  given(slackClient.conversationsInfo((RequestConfigurator<ConversationsInfoRequestBuilder>) any()))
    .willReturn(setUpChannelMockData(channel));

  // when
  channelCreatedService.execute(request);

  // then
  Optional<Channel> actual = channels.findBySlackId(channel.getSlackId());
  assertThat(actual).isNotEmpty();
}

private ConversationsInfoResponse setUpChannelMockData(final Channel channel) {
  Conversation conversation = new Conversation();
  conversation.setId(channel.getSlackId());
  conversation.setName(channel.getName());

  ConversationsInfoResponse conversationsInfoResponse = new ConversationsInfoResponse();
  conversationsInfoResponse.setChannel(conversation);
  conversationsInfoResponse.setOk(true);

  return conversationsInfoResponse;
}

TO-BE (์Šคํ… ์ ์šฉ)


@Test
void execute() {
  // given
  Workspace workspace = workspaces.save(WorkspaceFixture.JUPJUP.create());
  Channel channel = ChannelFixture.QNA.create(workspace);
  String request = createRequest(channel);

  // when
  channelCreatedService.execute(request);

  // then
  Optional<Channel> actual = channels.findBySlackId(channel.getSlackId());
  assertThat(actual).isNotEmpty();
}



๐Ÿ˜‰ ๋งˆ๋ฌด๋ฆฌ

ํ•ด๋‹น ๊ธ€์—์„œ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ๊ฐ„ํŽธํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์„œ๋น„์Šค ์ถ”์ƒํ™”๋ฅผ ์ง„ํ–‰ํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์„œ๋น„์Šค ์ถ”์ƒํ™”๋Š” ์ด ์™ธ์—๋„ ๋‹ค์–‘ํ•œ ๋ชฉ์ ์œผ๋กœ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐ์ฒด ์ง€ํ–ฅ์ ์ธ ์ฝ”๋“œ๋Š” ๋‹จ์ผ ์ฑ…์ž„ ์›์น™์„ ์ค€์ˆ˜ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด์˜ ChannelCreateService์—์„œ๋Š” โ€˜Slack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ˜ธ์ถœโ€™, โ€˜์˜ˆ์™ธ ์ฒ˜๋ฆฌโ€™, โ€˜์ฑ„๋„ ์ €์žฅโ€™ 3๊ฐ€์ง€ ์—ญํ• ์„ ํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฐœ์„ ๋œ ChannelCreateService์—์„œ๋Š” SlackClient๋ฅผ ํ†ตํ•ด ์ฑ„๋„ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์ด๋ฅผ DB์— โ€˜์ €์žฅโ€™ํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. โ€˜Slack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ˜ธ์ถœโ€™๊ณผ ์ด๋กœ ์ธํ•œ โ€˜์˜ˆ์™ธ ์ฒ˜๋ฆฌโ€™์˜ ์ฑ…์ž„์€ SlackClient์œผ๋กœ ์˜ฎ๊ฒจ๊ฐ”์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๋“ฏ ์„œ๋น„์Šค๋ฅผ ์ถ”์ƒํ™”ํ•˜๋ฉด ์ด์ „๋ณด๋‹ค ์ฝ”๋“œ๊ฐ€ ๊ฐ„๊ฒฐํ•ด์ง€๊ณ  ์ž‘์—…์˜ ๋ชฉ์ ์ด ๋ถ„๋ช…ํ•˜๊ฒŒ ๋“œ๋Ÿฌ๋‚ฉ๋‹ˆ๋‹ค.

๊ฐ ์˜ค๋ธŒ์ ํŠธ์˜ ์ฑ…์ž„๊ณผ ์—ญํ• ์ด ๋” ๋ถ„๋ช…ํ•˜๊ฒŒ ๋ถ„๋ฆฌ๋˜๋ฉด ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ์ƒ๊ธฐ๋Š” ๊ฒฝ์šฐ ๋ณ€๊ฒฝ ํฌ์ธํŠธ๋ฅผ ์‰ฝ๊ฒŒ ์ฐพ์•„๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ Slack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ๊ด€๋ จ๋œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ChannelCreateService๊ฐ€ ์•„๋‹Œ SlackClient๋ฅผ ์‚ดํ”ผ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์˜์กด๋˜๋Š” ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ Slack์ด ์•„๋‹Œ ์นด์นด์˜คํ†ก, ๋ผ์ธ ๋“ฑ์œผ๋กœ ๋ฐ”๋€” ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ์—๋„ ExternalClient์˜ ๊ตฌํ˜„์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ์‰ฝ๊ฒŒ ๋Œ€์ฒดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.




Reference & Source

  • ํ† ๋น„์˜ ์Šคํ”„๋ง 3.1 / ์ด์ผ๋ฏผ
  • ๋Œ€ํ‘œ ์ด๋ฏธ์ง€: ํ”ผ์—ํŠธ ๋ชฌ๋“œ๋ฆฌ์•ˆ์˜ ์ถ”์ƒํ™” ์ž‘ ๋ธŒ๋กœ๋“œ์›จ์ด ๋ถ€๊ธฐ์šฐ๊ธฐ