์๋น์ค ์ถ์ํ
๐ ์๋ก
์ฐ์ํํ
ํฌ์ฝ์ค์์ ์ฌ์ฉํ๋ ํ์
๋๊ตฌ ์ค ํ๋์ธ Slack
์ ๋ฌด๋ฃ ํ๋ฆฌํฐ์ด ์ฌ์ฉ ์ 3๊ฐ์์ด ์ง๋ ๋ฉ์์ง๋ค์ ๋ณด์ฌ์ฃผ์ง ์์ต๋๋ค.
๋ ๋ฒจ 3 ํ ํ๋ก์ ํธ์ธ ์ค์ค์์๋ ์ด ์ฌ๋ผ์ง๋ ๋ฉ์์ง๋ค์ ๋ฐฑ์
ํด์ฃผ๋ ์๋น์ค๋ฅผ ์ ๊ณตํฉ๋๋ค.
Slack
์ ๋ฉ์์ง์ ๋ํ ์ ๋ณด๋ฅผ ์ฃผ๋ก ๋ค๋ฃจ๋ค ๋ณด๋ Slack ๋ผ์ด๋ธ๋ฌ๋ฆฌ
์ ์์กด์ ์ธ ๋ก์ง์ด ๋ง์ต๋๋ค.
์ด ํฌ์คํ
์์๋ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(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();
}
}
}
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();
}
}
}
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();
}
}
}
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 / ์ด์ผ๋ฏผ
- ๋ํ ์ด๋ฏธ์ง: ํผ์ํธ ๋ชฌ๋๋ฆฌ์์ ์ถ์ํ ์
๋ธ๋ก๋์จ์ด ๋ถ๊ธฐ์ฐ๊ธฐ