springboot-mobile클라이언트 구별하기

들어가기

스프링에서 리퀘스트를 요청하는 클라이언트가 desktop인지 mobile인지 등을 구별하는 내용을 정리한다. 데스크탑 브라우저의 요청은 데스크탑용 뷰를, 모바일인 브라우저의 요청에는 모바일용 뷰를 응답하는 예제이다.

주의

예제는 spring boot 2버전이다. 사용할 라이브러리는 spring-mobile-starter 라는 라이브러리인데, 이 라이브러리는 springboot 2.x 버전이 되면서 deprecated가 되어 자동으로 버전관리가 되지 않는다.

이 예제는 springboot2.x 버전에서 spring-mobile-starter를 강제로 주입해서 사용하고 있다. 현재 springboot2.x에서 spring-mobile-starter를 대체하는 라이브러리나 기술을 확인하지 못하고 있다. 아시는분 댓글좀...

라이브러리 추가 및 설정

앞서 말했듯이 실습환경은 java8, springboot2, spring5, maven 환경이다.

pom.xml 아래 내용을 추가하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>org.springframework.mobile</groupId>
<artifactId>spring-mobile-starter</artifactId>
<version>2.0.0.M2</version>
</dependency>

<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>

springboot 1.5버전 대에서는 자동으로 버전관리가 되지만, springboot 2버전부터 제외된 라이브러리라 강제로 적용하였다.

application.profiles 파일에 아래 내용을 추가하자

1
2
3
4
spring.mobile.devicedelegatingviewresolver.enabled:true
spring.mobile.devicedelegatingviewresolver.normal-prefix=normal/
spring.mobile.devicedelegatingviewresolver.mobile-prefix=mobile/
spring.mobile.devicedelegatingviewresolver.tablet-prefix=tablet/

맨위 설정은 요청의 종류에 따라서 뷰 리졸버의 접두어로 경로를 붙이는 것을 활성화 하는 것이다. 그리고 그 아래부터 normal 요청에는 뷰 리졸버의 앞에 normal이라는 경로를 추가한다는 것이다. 그 아래는 mobile인경우, 또 그 아래는 tablet인 경우의 설정이다.

controller 설정

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
32
33
34
35
36
37
38
39
40
package hanumoka.portfolio.nao.web;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.mobile.device.Device;
import org.springframework.mobile.device.DeviceUtils;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;


import javax.servlet.http.HttpServletRequest;

@Controller
public class HomeController {

Logger logger = LoggerFactory.getLogger(HomeController.class);

@GetMapping(value={"/", "/index"})
public String index(Device device, Model model, HttpServletRequest request){

Device deviceFromRequest = DeviceUtils.getCurrentDevice(request);

if (device.isMobile()) {
logger.info("Hello mobile user!");
} else if (device.isTablet()) {
logger.info("Hello tablet user!");
} else {
logger.info("Hello desktop user!");
}

logger.info("device: " + device);
logger.info("device form request: " + deviceFromRequest);
logger.info("device platform: " + device.getDevicePlatform());

model.addAttribute("name", "hanumoka");
return "index";
}

}

컨트롤러의 device 객체를 통해서 클라이언트의 종류를 확인 할 수 있다. 내부적으로 http 요청 헤더의 user agent 값을 통해서 구별한다. application.properties의 설정으로 인해 http 요청에 따라 다른 경로의 index.html을 응답하게 된다. 예를 들어 일반 컴퓨터의 브라우저에서 위 컨트롤러에 접근한다면 /normal/index.html이 모바일 브라우저에서 위 컨트롤러로 접근한다면 /mobile/index.html이 테블릿 브라우저에서 위 컨트롤러로 접근한다면 /tablet/index.html을 응답하게 된다.

뷰 파일 구성

폴더구조와 html파일들

nomal, mobile, tablet 폴더를 만들고 그 내부의 index.html 파일을 생성했다. 참고로 이 예제는 thymeleaf가 적용되어있지만, 무시해도 상관 없다.

실행결과

간단하게 크롬브라우저에서 테스트 했다.

일반 브라우저에서 접근

모바일 브라우저에서 접근

테블릿 브라우저에서 접근

spring-mobile-starter 테스트 예제

참고로 spring-mobile-starter가 적용된 컨트롤러 테스트 코드를 추가한다. 주의 할 점은 WebMvcTest 어노테이션으로는 Device 객체를 생성하지 못해 테스트를 할 수가 없다. SpringBootTest, AutoConfigureMockMvc 어노테이션으로 테스트를 할 수 있다. 그리고 요청의 header의 user agent 값을 변경하여 결과를 확인 할 수 있다.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package hanumoka.portfolio.nao;

import hanumoka.portfolio.nao.web.HomeController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.core.StringContains.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
//@WebMvcTest(HomeController.class)
public class HomeControllerTest {

@Autowired
MockMvc mockMvc;

@Before
public void setup(){

}

@Test
public void hello() throws Exception{
mockMvc.perform(get("/")
.header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36")
.accept(MediaType.TEXT_HTML))
.andExpect(status().isOk())
.andDo(print())
.andExpect(view().name("index"))
.andExpect(model().attribute("name", is("hanumoka")));

/* mockMvc.perform(get("/")
.header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36")
.accept(MediaType.TEXT_HTML))
.andExpect(view().name(containsString("desktop")))
.andExpect(status().isOk())
.andDo(print())
.andExpect(view().name("index"))
.andExpect(model().attribute("name", is("hanumoka")));*/
}


}

참고자료

https://stackoverrun.com/ko/q/10317311

springboot2.0 spring mobile 디펜던시 https://stackoverflow.com/questions/53133226/how-to-use-device-resolver-for-spring-boot-2-00

http://cpdev.tistory.com/50

http://codingdojang.com/scode/297

https://www.slipp.net/questions/439

http://arahansa.github.io/docs_spring/device.html

https://medium.com/@hun/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-bf6e03de29fc

https://coding-factory.tistory.com/32