uncategorized

광운대학교 학생 구성원 인증은 어떻게 할까?

문제 상황

광운대학교 미디어커뮤니케이션 학부 기자재 대여 웹 서비스를 구현할 때 광운대학교 미디어커뮤니케이션 학생만 사용할 수 있도록 해야한다. 하지만 문제는 광운대학교에서는 Open API를 제공하지 않아서 학생 구성원을 어떻게 확인해야 할 지가 큰 문제였다.

개선 방법

광운대학교 학습 전산 시스템인 klas에 가보면 개인번호 조회 기능이 있는 걸 발견했다. 이름과 생년월일을 입력하면 신분, 학과, 성명, 개인번호를 조회할 수 있다. 기리기리 팀은 이를 활용해보기로 했다.

그렇다면 광운대학교의 개인번호 조회 API를 얻어낼 수 있지 않을까? 크롬 개발자 도구를 통해 네트워크 탭에서 어떤 요청을 보내는 지 확인해봤다. 그 결과 다음과 같은 API를 통해 요청을 날리는 것을 확인할 수 있었다.

자 그러면 해당 API를 포스트맨으로 테스트해보자.

이 정보를 활용하면 미디어커뮤니케이션 구성원 검증을 할 수 있을 것이다!!!

어디서 API 호출할 것인가?
우리가 찾아낸 API를 브라우저(프론트엔드)에서 즉시 호출하면 CORS 에러가 발생한다!!!!! 왜냐면 해당 요청의 Content-Type이 application/json인 POST 요청이라 (즉 non simple request) CORS 정책에 대상이 되는 요청이기 때문이다. 이렇기 때문에 백엔드에서 API를 호출하고 프론트엔드에 전달하는 방식으로 구현하기로 했다.

구현

이제 백엔드에서 광운대 API를 호출해서 프론트엔드에 학번 정보만 전달하는 코드를 구현해보자.

Spring 6 HTTP interface 구현

Spring 6에서 HTTP interface 기능을 활용해서 HTTP Client를 쉽게 구현할 수 있다. 먼저 HTTP interface는 내부적으로 WebClient를 사용하므로 WebFlux를 의존성 추가해야 한다.

1
implementation 'org.springframework.boot:spring-boot-starter-webflux'

그리고 광운대학교 API에 해당하는 DTO를 만들어본다. Java 14의 Record를 활용한다. 필드명은 광운대학교 API에 맞춰서 선언했다.

1
2
3
4
public record KwangwoonMemberResponse(
String gubun, String codeName1, String sex, String hakbun, String sortNo
) {
}
1
2
3
4
5
public record KwangwoonMemberRetrieveRequest(
String name, String birthday
) {
}

그리고 광운대하교 API를 호출할 Http interface를 구현해보자.

1
2
3
4
5
6
public interface KwangwoonMemberService {

@PostExchange("/ext/out/SelectFindAllList.do")
List<KwangwoonMemberResponse> retrieve(
@RequestBody final KwangwoonMemberRetrieveRequest kwangWoonMemberRetrieveRequest);
}

@PostExchange는 해당 URL로 post 요청을 처리하는 메서드를 의미한다. 스프링은 이 인터페이스를 기반으로 WebClient를 사용해서 구현체를 만들게 된다. 다만 이 인터페이스의 프록시 객체를 직접 빈으로 등록해줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class WebConfig implements WebMvcConfigurer {

// 생략...
@Bean
public KwangwoonMemberService kwangwoonMemberService() {
final WebClient kwangwoonServerClient = createKwangwoonServerClient();
final HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(kwangwoonServerClient))
.build();
return factory.createClient(KwangwoonMemberService.class);
}

private WebClient createKwangwoonServerClient() {
return WebClient.builder()
.baseUrl("https://klas.kw.ac.kr/")
.defaultStatusHandler(HttpStatusCode::isError, res -> Mono.just(new KwangwoonServerException()))
.build();
}
}

광운대 API의 baseUrl과 예외 핸들링을 설정한 WebClient를 생성하고 HttpInterface의 구현체로 만들어 빈으로 등록한다. 그러면 이제 아까 만든 인터페이스를 주입할 때 해당 구현체를 주입할 것이다.

Http interface 주입받아서 광운대 학생 검증 로직 구현

이제 광운대 API를 통해 학생의 학번과 학과 등 정보를 조회해올 수 있다. 우리의 목표는 해당 학생이 미디어커뮤니케이션 학생인지를 검증하는 것이다. 미디어커뮤니케이션 학생들은 10자리 학번 중 가운데 3자리가 317 혹은 323이다. 이를 확인하면 광운대 미디어커뮤니케이션 학생임을 확인할 수 있다.

enum으로 미디어영상학부 전공 번호를 도입해서 미디어영상학부 전공번호 확인 로직을 분리했다. 이렇게 한 이유는 전공 번호 확인 로직을 분리해야 테스트하는데 유리하고 학번에서 전공번호의 인덱스 등을 서비스에서 노출하는 것이 지나치게 세부적인 사항이라고 판단했다!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public enum MediaMajorNumber {
OLD_MEDIA_COMMUNICATION("317"),
MEDIA_COMMUNICATION("323");

private static final int MEMBER_NUMBER_LENGTH = 10;
private static final int MAJOR_BEGIN_INDEX = 4;
private static final int MAJOR_END_INDEX = 7;

private final String value;

MediaMajorNumber(final String value) {
this.value = value;
}

public static boolean isMediaMajor(final String memberNumber) {
validateMemberNumberLength(memberNumber);
final String majorNumber = extractMajorNumber(memberNumber);
return Arrays.stream(values())
.anyMatch(it -> it.value.equals(majorNumber));
}

private static void validateMemberNumberLength(final String memberNumber) {
if (memberNumber.length() != MEMBER_NUMBER_LENGTH) {
throw new MemberNumberException("조회된 학번이 10글자가 아닙니다.");
}
}

private static String extractMajorNumber(final String memberNumber) {
return memberNumber.substring(MAJOR_BEGIN_INDEX, MAJOR_END_INDEX);
}
}

지금까지 구현한 컴포넌트를 호출하는 서비스를 구현하자. 기존의 회원 관리 서비스와 분리해서 서비스를 구현했는데, 기존의 회원 서비스는 현재 기리기리 서비스의 회원과 관련된 서비스고 지금 구현하려는 광운대 학생 확인 서비스는 아직 우리의 회원이 아닌 사람의 정보를 다루는 로직이라 판단해서 구분지었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Service
@RequiredArgsConstructor
public class MemberNumberRetrieveService {

private final KwangwoonMemberService kwangwoonMemberService;

public MemberNumberResponse retrieveMediaMemberNumber(
final KwangwoonMemberRetrieveRequest kwangwoonMemberRetrieveRequest) {
final String memberNumber = getMemberNumber(kwangwoonMemberRetrieveRequest);
validateMajorNumber(memberNumber);
return new MemberNumberResponse(memberNumber);
}

private String getMemberNumber(final KwangwoonMemberRetrieveRequest kwangwoonMemberRetrieveRequest) {
final List<KwangwoonMemberResponse> kwangwoonMemberResponses = kwangwoonMemberService.retrieve(
kwangwoonMemberRetrieveRequest);
if (kwangwoonMemberResponses.size() != 1) {
throw new MemberNumberRetrieveException("조회 결과가 단일하지 않습니다.");
}
return kwangwoonMemberResponses.iterator().next().hakbun();
}

private void validateMajorNumber(final String memberNumber) {
final boolean isMediaMajor = MediaMajorNumber.isMediaMajor(memberNumber);
if (!isMediaMajor) {
throw new MemberNumberRetrieveException("미디어커뮤니케이션 학번이 아닙니다.");
}
}
}

위 코드에서 아래와 같이 작성만 하면 HTTP 요청을 보내서 응답을 받을 수 있다. 서비스 로직 작성자는 해당 서비스가 레포지토리를 통해 정보를 조회하는 지, HTTP를 통해 가져오는 지 세부 구현에 대한 관심을 제거할 수 있다.

1
final List<KwangwoonMemberResponse> kwangwoonMemberResponses = kwangwoonMemberService.retrieve(kwangwoonMemberRetrieveRequest);

마지막으로 해당 서비스를 호출하는 클래스를 구현하면 작업이 끝난다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

private final MemberNumberRetrieveService memberNumberRetrieveService;

@GetMapping(path = "/memberNumbers", params = {"name", "birthday"})
public MemberNumberResponse getMemberNumberFromKLAS(
final KwangwoonMemberRetrieveRequest kwangwoonMemberRetrieveRequest) {
return memberNumberRetrieveService.retrieve(kwangwoonMemberRetrieveRequest);
}
}

결과

우리가 가진 문제를 광운대하교 서비스를 분석해서 API를 찾아냈다. 그리고 백엔드 서버에서 Spring 6 Http interface를 활용해서 쉽게 광운대 API를 활용해서 미디어커뮤니케이션 학부 인증 API를 구현할 수 있었다.

Share