0%

Java에서 어노테이션(Annotation) 이란?

자바 개발을 하다 보면 클래스 또는 메서드 또는 변수 앞에 @Override 같은 @ 표시를 많이 봤을 것입니다.
이 어노테이션은 JEE5(Java Platform, Enterprise Edition 5)부터 새롭게 추가된 요소입니다.

이 어노테이션으로 인해 데이터의 유효성 검사 등을 쉽게 알 수 있고, 이와 관련한 코드가 깔끔해지게 됩니다.
일단 어노테이션의 용도는 다양한 목적이 있지만 메타 데이터의 비중이 가장 크다 할 수 있습니다.

메타-테이터(Meta-Data) : 데이터를 위한 데이터를 의미하며, 풀어 이야기하면 한 데이터에 대한 설명을 의미하는 데이터. (자신의 정보를 담고 있는 데이터)

Java에서 기본적으로 제공하는 어노테이션 종류

  1. @Override
  • 선언한 메서드가 오버라이드 되었다는 것을 나타냅니다.
  • 만약 상위(부모) 클래스(또는 인터페이스)에서 해당 메서드를 찾을 수 없다면 컴파일 에러를 발생 시킵니다.
  1. @Deprecated
  • 해당 메서드가 더 이상 사용되지 않음을 표시합니다.
  • 만약 사용할 경우 컴파일 경고를 발생 키십니다.
  1. @SuppressWarnings
  • 선언한 곳의 컴파일 경고를 무시하도록 합니다.
  1. @SafeVarargs
  • Java7 부터 지원하며, 제너릭 같은 가변인자의 매개변수를 사용할 때의 경고를 무시합니다.
  1. @FunctionalInterface
  • Java8 부터 지원하며, 함수형 인터페이스를 지정하는 어노테이션입니다.
  • 만약 메서드가 존재하지 않거나, 1개 이상의 메서드(default 메서드 제외)가 존재할 경우 컴파일 오류를 발생 시킵니다.

어노테이션은 어떻게 구성되어 있을까?

먼저 어노테이션의 구조를 보기 위해서 아래의 코드를 참고해 봅니다. (예를 위해 작성된 커스텀 어노테이션입니다.)

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotation {
boolean isCheck() default true;
}

먼저 1번, 2번의 어노테이션 선언에 쓰인 어노테이션은 **메타 어노테이션(Meta Annotation)**이라 하며 이를 이용해 커스텀 어노테이션을 작성할 수 있게 됩니다.

메타 어노테이션의 종류

  1. @Retention
  • 자바 컴파일러가 어노테이션을 다루는 방법을 기술하며, 특정 시점까지 영향을 미치는지를 결정합니다.
  • 종류는 다음과 같습니다.
    • RetentionPolicy.SOURCE : 컴파일 전까지만 유효. (컴파일 이후에는 사라짐)
    • RetentionPolicy.CLASS : 컴파일러가 클래스를 참조할 때까지 유효.
    • RetentionPolicy.RUNTIME : 컴파일 이후에도 JVM에 의해 계속 참조가 가능. (리플렉션 사용)
  1. @Target
  • 어노테이션이 적용할 위치를 선택합니다.
  • 종류는 다음과 같습니다.
    • ElementType.PACKAGE : 패키지 선언
    • ElementType.TYPE : 타입 선언
    • ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언
    • ElementType.CONSTRUCTOR : 생성자 선언
    • ElementType.FIELD : 멤버 변수 선언
    • ElementType.LOCAL_VARIABLE : 지역 변수 선언
    • ElementType.METHOD : 메서드 선언
    • ElementType.PARAMETER : 전달인자 선언
    • ElementType.TYPE_PARAMETER : 전달인자 타입 선언
    • ElementType.TYPE_USE : 타입 선언
  1. @Documented
  • 해당 어노테이션을 Javadoc에 포함시킵니다.
  1. @Inherited
  • 어노테이션의 상속을 가능하게 합니다.
  1. @Repeatable
  • Java8 부터 지원하며, 연속적으로 어노테이션을 선언할 수 있게 해줍니다.

어노테이션은 기본적으로 인터페이스 형태를 취하고 있으며, 단지 interface 앞에 @ 표시를 해줍니다.
어노테이션의 필드에서는 enum, String이나 기본 자료형, 기본 자료형의 배열을 사용할 수 있습니다.

어노테이션에 대한 이론적인 부분은 여기까지이며, 이를 활용하여 커스텀 어노테이션을 작성하는 것은 다음 포스트에서 다루도록 하겠습니다.

커스텀 어노테이션 만들고 사용하기

19년 9월 8일 추가

19년 9월 8일 Github에 해당 소스를 등록하였다.
Github이곳을 참고하면 된다.


커스텀 어노테이션을 만들어 보자.

Java에서 어노테이션(Annotation)이란?
포스팅에서 어노테이션에 대해 간단히 알아보았습니다.

이번 포스팅에서는 직접 커스텀 어노테이션을 작성하는 방법에 대하여 알아보도록 하겠습니다.
두 가지의 예제를 통해 알아보도록 하겠습니다.


1. 정수 값 주입 예제

처음 주제는 어노테이션을 선언한 정수형 변수에 값을 넣는 예제를 진행해보겠습니다.

간단한 예제 이므로 주석은 달지 않거나 간단한 설명으로 대체하겠습니다.

1. 어노테이션 인터페이스 작성

1
2
3
4
5
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InsertIntData {
int data() default 0;
}

인터페이스를 만들어주는데 앞에 @ 표시를 붙이면 됩니다.
1번과 2번에 대한 설명은 어노테이션 포스팅에서 정리하였습니다.
일단 멤버 변수에 data라는 주입을 받을 값을 만들어 줍니다.

2. 어노테이션을 사용할 예제 클래스 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AnnotationExam01 {
@InsertIntData(data = 30)
private int myAge;

@InsertIntData
private int defaultAge;

public AnnotationExam01() {
this.myAge = -1;
this.defaultAge = -1;
}

public int getMyAge() {
return myAge;
}

public int getDefaultAge() {
return defaultAge;
}
}
}

변수는 다음과 같이 myAgedefaultAge 두 가지인데 myAge에 어노테이션에서는 30으로 값을 주입합니다.
하지만 defaultAge 에서는 값이 없는데 이 경우 어노테이션에서 정한 기본 값인 0으로 값이 주입이 됩니다.
생성자의 경우 값이 없을 경우 -1을 기본으로 저장합니다.

다음은 두 번째 예제인 문자열 값 주입을 보도록 하겠습니다.
두 번째 예제에서는 수행 클래스 및 실행 클래스까지 알아보겠습니다.


2. 문자열 값 주입 예제

두 번째 주제는 어노테이션을 선언한 정수형 변수에 값을 넣는 예제를 진행해보겠습니다.

간단한 예제 이므로 주석은 달지 않거나 간단한 설명으로 대체하겠습니다.

1. 어노테이션 인터페이스 작성

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
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InsertStringData {
String data() default "default";
}```

위의 1-1와 비슷합니다.
기본 값으로는 **default** 문자열을 가집니다.


### 2. 어노테이션을 사용할 예제 클래스 작성

```java
public class AnnotationExam02 {

@InsertStringData(data = "MHLab")
private String myData;

@InsertStringData
private String defaultData;

public AnnotationExam02() {
myData = "No data";
defaultData = "No data";
}

public String getMyData() {
return myData;
}

public String getDefaultData() {
return defaultData;
}
}

변수는 다음과 같이 myDatadefaultData 두 가지인데 myData에 어노테이션에서는 “MHLab”으로 값을 주입합니다.
하지만 defaultData 에서는 값이 없는데 이 경우 어노테이션에서 정한 기본 값인 “default”로 값이 주입이 됩니다.
생성자의 경우 값이 없을 경우 “No data” 문자열을 기본으로 저장합니다.

3. 어노테이션을 수행하는 클래스 작성

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
public class AnnotationHandler {
private <T> T checkAnnotation(T targetObj, Class annotationObj) {
Field[] fields = targetObj.getClass().getDeclaredFields();
for (Field f : fields) {
if(annotationObj == InsertIntData.class) {
return checkAnnotation4InsertInt(targetObj, f);
}
else if(annotationObj == InsertStringData.class) {
return checkAnnotation4InsertString(targetObj, f);
}
}
return targetObj;
}

private <T> T checkAnnotation4InsertInt(T targetObj, Field field) {
InsertIntData annotation = field.getAnnotation(InsertIntData.class);
if(annotation != null && field.getType() == int.class) {
field.setAccessible(true);
try { field.set(targetObj, annotation.data()); }
catch (IllegalAccessException e) { System.out.println(e.getMessage()); }
}
return targetObj;
}

private <T> T checkAnnotation4InsertString(T targetObj, Field field) {
InsertStringData annotation = field.getAnnotation(InsertStringData.class);
if(annotation != null && field.getType() == String.class) {
field.setAccessible(true);
try { field.set(targetObj, annotation.data()); }
catch (IllegalAccessException e) { System.out.println(e.getMessage()); }
}
return targetObj;
}

public <T> Optional<T> getInstance(Class targetClass, Class annotationClass) {
Optional optional = Optional.empty();
Object object;
try {
object = targetClass.newInstance();
object = checkAnnotation(object, annotationClass);
optional = Optional.of(object);
}catch (InstantiationException | IllegalAccessException e) { System.out.println(e.getMessage()); }
return optional;
}
}

약간 코드가 복잡한데 하나씩 설명드리겠습니다.
(코드 리펙토링이 필요하지만 예제를 위한 코드이기에 그냥 진행하겠습니다.)

getInstance 메서드

이 메서드는 두 가지의 전달인자를 받습니다.
첫 번째는 어노테이션이 적용되어 있는 2번에서 작성한 클래스, 두 번째는 체크할 어노테이션 클래스입니다.
반환 값은 Optional을 사용하여 반환하게 됩니다.
먼저 타겟 클래스의 인스턴스를 생성하고, checkAnnotation 메서드를 호출합니다.
여기서 전달인자에 어노테이션 클래스를 넣은 것은 향후 확장성을 고려 하였는데, 이 부분은 기호에 알맞게 메서드를 나눠서 구현을 해도 무방합니다.

checkAnnotation 메서드

이 메서드는 앞선 getInstance 메서드의 전달인자를 그대로 받습니다.
fields 변수는 타겟 객체에 선언된 것들을 모두 가져옵니다. (Field는 리플렉션과 관련되어 있고, 이는 다음 포스팅에서 다루겠습니다.)
그 다음 전달인자 annotationObj 값에 따라 분기를 나눠 메서드를 호출하게 됩니다.

checkAnnotation4InsertInt 메서드 (checkAnnotation4InsertString 메서드도 동작이 비슷하게 여기 설명으로 대체합니다.)

해당 메서드는 한 라인씩 간략하게 짚고 넘어가겠습니다

InsertIntData annotation = field.getAnnotation(InsertIntData.class);
이 부분은 전달인자로 받은 Field에서 선언된 어노테이션을 가져옵니다.

if(annotation != null && field.getType() == int.class)
이 부분은 어노테이션이 null이 아니거나 선언된 변수의 타입이 int형일 경우에만 수행을 하게 됩니다.

field.setAccessible(true);
일반적으로 private로 선언된 변수(필드)의 경우 접근이 불가능하지만, 리플렉트를 통한 접근에 한하여 가능하게끔 해준다.

try { field.set(targetObj, annotation.data()); }
해당 변수의 값을 어노테이션의 값으로 치환하게 됩니다.

위와 같은 작업을 거치고 난 후 전달인자로 넘어온 어노테이션이 선언된 클래스 객체를 반환하게 됩니다.


4. 실행 클래스 작성

아래는 위의 두 가지 예제의 실행코드를 작성하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
AnnotationHandler handler = new AnnotationHandler();
AnnotationExam01 exam01 = handler.getInstance(AnnotationExam01.class, InsertIntData.class)
.map(o -> (AnnotationExam01)o)
.orElse(new AnnotationExam01());

AnnotationExam02 exam02 = handler.getInstance(AnnotationExam02.class, InsertStringData.class)
.map(o -> (AnnotationExam02)o)
.orElse(new AnnotationExam02());

System.out.println("myAge = " + exam01.getMyAge());
System.out.println("defaultAge = " + exam01.getDefaultAge());

System.out.println("myData = " + exam02.getMyData());
System.out.println("defaultData = " + exam02.getDefaultData());
}

실행결과는 다음과 같이 출력됩니다.

myAge = 30
defaultAge = -1
myData = MHLab
defaultData = No data
Process finished with exit code 0


정리

뭔가 글이 복잡하고 장황하게 쓴 것 같지만…최대한 내용을 쉽게 풀어서 작성하였습니다.
잘못되었거나 문제가 있는 부분은 알려주시면 수정하도록 하겠습니다.

커스텀 어노테이션의 활용

제가 진행하는 프로젝트에서는 안드로이드와 Spring Boot 서버의 서로 통신을 진행하면서 Jwt를 이용하여 사용자 인증을 진행하였습니다.
이번 포스팅에서는 Jwt인증에 대한 부분 보다는,
커스텀 어노테이션을 왜 쓰게 되었는지와 더불어 어떤식으로 커스텀 어노테이션을 활용하였는지에 대해 작성하도록 하겠습니다.


왜 커스텀 어노테이션을 쓰게 되었는가?

서버와 모바일에서 사용자 인증은 jwt를 활용하고 있습니다.
컨트롤러에 진입할 때 Header에 있는 jwt를 받아서 해당 jwt를 검증하는 방식을 사용하고 있습니다.
처음에는 로직을 너무 복잡하게 짰습니다.

초창기에 개발된 일부 코드를 보도록 하겠습니다.

1
2
return isProblemJsonResultCode(securityService.veryfiyHeaderInJwt(request))
.orElseGet(() -> {...})

위의 작은 코드에서만 봐도 jwt를 검증하는 로직 자체도 불분명하고, 람다식을 잘못 쓴 것도 보이네요.
컨트롤러에 진입할 때마다 저런식으로 작업을 하게 된다면 코드가 정말 지저분해질 것으로도 예상되네요. (실제로도 그랬습니다…)

그래서 이 부분을 어떻게 개선을 할까 하다가 인터셉터를 활용하기로 하였습니다.
특정 url에 접근하게 된다면 인터셉터에서 처리하는 방식을 사용하는 것을 생각하였습니다.
그런데 이 부분에서는 아래와 같은 고민거리에 대해 생각하게 되었습니다.

  1. 특정 Rest api 호출 시에는 jwt 검증이 필요하지 않다.
  2. 서버가 Rest api only가 아니라서 몇 가지 다른 url 호출도 같이 쓰게 됨

1번의 경우 검증을 안하는 api가 적은 숫자는 아니라서 excludePathPatterns 을 일일히 적용하기엔 인터셉터가 길어질 것 같았습니다.
2번의 경우 기존에 개발이 된 서버에 Rest api를 붙이는 방식이어서(사실 설계측도 잘못되어 있지만..) url 앞 부분이 겹치는 문제도 있었습니다.

그래서 이 부분을 해결하게 된 것이 커스텀 어노테이션이었습니다.
컨트롤러에서 jwt 검증을 진행해야 하는 부분에는 @CheckJwt 와 같은 어노테이션을 붙여서 구분을 하게 하는 방식을 생각하였습니다.


어떤 식으로 활용을 하였는가?

일단 어노테이션과 커스텀 어노테이션에 대한 이해가 있다는 가정하에 진행하겠습니다.

만약 두 가지에 대해 모르신다면 아래의 포스팅을 참고해주세요.
Java에서 어노테이션(Annotation)이란?
커스텀 어노테이션 만들고 사용하기

  1. 인터셉터를 하나 만든다.

먼저 인터셉터를 하나 만들어줍니다.

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckJwt {
boolean needCheck() default true; //체크가 필요한 경우의 여부 (기본 값 : 검사)
}

이런식으로 만들어줍니다.

  1. 선언된 인터셉에서 어노테이션 처리를 진행

저같은 경우 컨트롤러 진입하기 전에 jwt 검증을 처리하기 위해서 preHandle에서 처리를 진행하였습니다.

1
2
3
4
5
6
7
8
9
HandlerMethod method = (HandlerMethod)handler;
CheckJwt checkJwt = method.getMethodAnnotation(CheckJwt.class);

if (checkJwt == null || checkJwt.needCheck()==false) { return true; }//검사를 할 필요가 없는 부분
else { //세션 체크 검사
return Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION))
.map(token -> jwtService.veryfiyJwt_new(token))
.orElseThrow(JwtNotFoundException::new); //토큰이 존재하지 않는 경우
}

preHandle의 전달인자 중 handler 파라메터는 인터셉터에서 핸들러 매핑이 찾아 준 컨트롤러의 객체를 반환합니다.
그래서 첫번 째 줄에서 method에는 호출된 컨트롤러의 객체가 담겨져 있고, 두번째 줄에서 @CheckJwt 어노테이션을 찾습니다.
만약 @checkJwt가 선언이 안되어 있거나 needCheck가 false일 경우 검사를 진행하지 않고, 그 외에는 검사를 진행하게 됩니다.

  1. 선언 및 사용

이제 @CheckJwt는 아래와 같이 사용하게 될 수 있습니다.

1
2
3
4
@CheckJwt
@ResponseBody
@GetMapping("list/{hour}/{level}")
public JsonResultVo getMachineWarning(HttpServletRequest request, @PathVariable("hour") String hour, @PathVariable("level") String level) { ... }

정리

자바의 커스텀 어노테이션을 활용한다면 정말 효과적인 코드 리펙토링을 진행할 수 있을 뿐더러, 코드의 이해도가 향상되는 것 같습니다.
다음에는 jwt에 대한 것에 대하여 알아보는 포스팅을 작성해보도록 하겠습니다.

감사합니다.

Spring Boot에서 HTTPS를 적용하려면?

Spring Boot환경에서 HTTP 통신으로 프로젝트를 구현하고 있었습니다.

하지만 보안적인 부분에 문제가 있어서 이 부분을 해결하기 위해 다양한 방법을 찾아봤습니다.

RSA로 암호화를 하는 방법을 주로 사용했었습니다.

하지만 이렇게 하는 것보다 그냥 HTTPS를 사용하여 처리하는 것이 좋을 것 같아서 이 부분에 대해 적용을 해보았고, 경험을 포스팅하려 합니다.

진행에 앞서 개발환경은 Mac OS X이고, Java가 설치되어 있는 환경에서 진행됩니다.


어떻게 진행을 해야 하는가?

먼저 다음의 스텝으로 진행을 합니다.

1. 키스토어 생성
2. 인증서 추출
3. Trust-Store 생성
4. Spring boot 환경 구성

위 순서대로 하나씩 진행을 해보겠습니다.


Key Store 생성하기

먼저 Key Store를 생성해줘야 합니다.

터미널을 열고 다음과 같이 명령어를 사용합니다.

keytool -genkey -alias (키스토어의 별칭) -keyalg RSA -keystore (생성할 키스토어의 파일명).jks

별칭은 KeyStore의 닉네임 같은 것이라 보면 되고, 이름은 KeyStore의 파일 명입니다.

실행을 하면 다음과 같이 KeyStore 정보를 입력하게 되고 생성이 완료됩니다.

KeyStore란?
KeyStore는 비밀키, 관련된 인증서 혹은 인증서 체인을 가지고 있는 데이타베이스입니다.
인증서 체인은 클라이언트 인증서와 하나 이상의 CA 인증서(공인인증서)로 구성됩니다.
보통 서버 역할을 하는 측에서만 필요하지만, 클라이언트 인증을 요구하는 경우 클라이언트 쪽도 있어야 합니다.


인증서 추출하기

Trust-Store를 생성하기 위해서는 1번에서 생성한 Key-Store에서 인증서를 추출해야 합니다.

아래의 명령어를 통해 인증서를 추출합니다.

keytool -export -alias (키스토어의 별칭) -keystore (키스토어의 파일명) -rfc -file (생성할 인증서 파일이름).cer


Trust-Store 생성하기

위에서 생성한 인증서를 통해서 Trust Store를 생성합니다.

keytool -import -alias (Trust-Store의 별칭) -file (인증서 파일명) -keystore (생성할 Trust-Store 파일명).ts

Trust Store란?
TrustStore는 클라이언트가 신뢰할 수 있는 인증서만을 가지고 있습니다.
이들 인증서는 CA 루트 인증서 즉 자기서명 인증서입니다.
보통 웹서버가 설치되면 cacerts.jks라는 파일명으로 이 TrustStore가 저장됩니다.


Spring boot 환경 구성하기

위에서 생성한 KeyStore, Trust-Store를 통해서 이제 Spring boot에서 SSL 구성을 해보도록 하겠습니다.

먼저 Spring boot에서 환경설정을 엽니다.

application.properties를 많이 사용하지만 저는 application.yml을 사용하기에 여기에 맞춰서 설명하겠습니다. (사실 둘다 비슷해서..)

1
2
3
4
5
6
7
8
9
10
server:
port: 8090 #Https port
ssl:
enabled: true
key-store: /home/test/mhlab_keystore.jks # 1번에서 생성한 Key store 경로와 파일명을 적어줍니다.
key-store-password: passwd # 1번에서 생성한 Key store의 비밀번호를 적어줍니다.
key-password: passwd # 1번에서 생성한 키스토어의 마지막의 키 비밀번호
key-alias: mhlab # 키 스토어 별칭
trust-store: /home/test/mhlab_keystore.ts # 2번에서 생성한 Trust-Store 경로
trust-store-password: passwd # Trust-Store 비밀번호

위와같이 구성 후 서버를 구동하고 브라우저에서 접속을 해봅니다. (크롬 브라우저 기준)

위와 같이 뜨는 이유는 생성한 인증서가 공식적으로 인증받지 않은 인증서이기 때문입니다.

이 부분은 향후 다시 포스팅 하기로 하고… 하단의 고급 버튼을 클릭하면…

이렇게 세부정보가 나오고 하단의 localhost(안전하지 않음) 을 클릭하면 페이지가 이동됩니다.

그리고 접속한 주소를 확인해보면 위와 같이 https로 표시됩니다.


결론

사실 지금까지 해본 플로우로 적용하는 것 자체는 쉬운 편입니다.(?)

하지만 저기에서 쓰인 SSL이나 인증서, Keystore, Trust-Store 및 Java의 Keytool 등에 대한 것에 대한 이해는 쉬운 편은 아니라 생각합니다.

다음 포스팅에서는 위와 같은 이론적인 측면을 포스팅해보도록 하겠습니다.

삽질만 6시간…실화임?

지금 생각해보면 하도 어이가 없어서 이 포스팅을 쓰기도 부끄럽습니다만…

(제 무지의 깊이가 상당히 깊다는 것도…반성의 의미로 작성하는 글)

오늘 봉착한 문제는 이것입니다.

데이터베이스에 값이 정상적으로 등록이 되지 않는 문제

이렇게 보니 정말 심플한 문제로 보일 것입니다.

좀 더 자세하게 들어가서 보자면…

Spring Boot 프로젝트의 Jpa를 사용하여 데이터베이스에 값을 넣는데 계속 문제가 발생하였습니다.

이렇게만 봐도 설정을 잘못했거나 미숙한 초보의 실수(물론 초보의 실수..)라 보이겠지만…

오늘의 실수와 교훈에 대해 시작하겠습니다.


문제와 원인 그리고…

문제의 DB 테이블은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `account` (
`idx` int(11) unsigned NOT NULL AUTO_INCREMENT,
`id` varchar(60) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
`pw` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`create_date` timestamp NOT NULL,
`email` varchar(60) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
`phone` varchar(20) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
`type` varchar(60) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
`group` varchar(60) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
`update_date` timestamp NOT NULL,
PRIMARY KEY (`idx`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

그리고 Entity객체는 다음과 같이 구성되어 있습니다.

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
@Entity
@ToString
@Accessors(chain = true)
@Getter @Setter
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int idx;

private String id;

private String pw;

private LocalDateTime createDate;

private String email;

private String phone;

private String userType;

private String userGroup;

private LocalDateTime updateDate;
}

그리고 jpa에서 데이터가 추가되는 로직은 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
accountRepo.save(
new Account()
.setId("admin")
.setPw(shaSecurityService.encryptData4SHA("admin"))
.setCreateDate(LocalDateTime.now())
.setEmail("admin@admin.com")
.setPhone("000-000-0000")
.setUserType("admin")
.setUserGroup("관리자")
.setUpdateDate(LocalDateTime.now())
);

이 구문을 수행할 때 에러가 발생하였습니다.

에러 구문은 다음과 같습니다.

1
2
3
4
5
6
7
8
2017-11-27 14:25:08 ERROR :[SqlExceptionHelper.java]logExceptions(129) : 
(conn:33312) You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version
for the right syntax to use near
'group, id, phone, pw, type, update_date) values ('2017-11-27 14:25:08.67', 'admi' at line 1

Query is: insert into account (create_date, email, group, id, phone, pw, type, update_date) values (?, ?, ?, ?, ?, ?, ?, ?),
parameters ['2017-11-27 14:25:08.67','admin@admin.com','관리자','admin','000-000-0000','c7ad44cbad762a5da0a452f9e854fdc1e0e7a52a'
,'admin','2017-11-27 14:25:08.67']

에러에서는 ‘admi’ at line 1 이 부분의 문제로 보고 제가 쿼리를 주는 Entitiy 부분의 문제인지도 확인하였지만 아니었습니다.

당췌 어디가 문제인지를 찾다가 하이버네이트의 SQL 로그를 자세히 확인해보기로 하였습니다.

그리고 원인을 찾게 된 것은 다음과 같았습니다…

데이터베이스에 테이블을 지우고, JPA에서 직접 테이블을 생성을 해보는 것을 시도하던 도중

group` varchar(60) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT ‘’

특수 이름을 사용한 경우에 발생하는 문제였던 것이었습니다….

Stackoverflow에서 찾은 내용입니다.


처참한 결과…

결국 테이블의 컬럼명을 바꿈으로써 문제를 해결하였습니다.

혹시 주니어 개발자 또는 초급 개발자의 경우 혹시 이런 문제가 발생한 경우 이것을 보고 도움이 되셨으면 합니다…

저처럼 시간을 많이 허비하지 않았으면 합니다…

해당 글은 11월 초중순 쯤 초안이 작성되었고, 해당 글을 정리하여 포스팅한 내용입니다.

UI와 UX의 고민

사용자와 관련된 모바일 어플리케이션 또는 웹 페이지를 개발할 때 우리는 고민을 하게 됩니다.

물론 큰 규모의 회사인 경우 UX를 전담으로 하는 팀이 있고, UI를 작업하는 디자이너가 있습니다.

하지만 작은 규모의 회사는 이런 인력을 충원하기 매우 어렵습니다.

일반적으로 제가 만나본 개발자들의 경우 디자인에 대한 고려를 전혀 하지 않았습니다.

또한 기능만 동작하면 되지, UX와 UI는 크게 신경을 쓸 필요가 없는 부분이라 생각을 합니다.

(제 개인적인 경험과 인맥에 제한된 것입니다. 절대 대다수 개발자를 이야기하는게 아닙니다.)

개인적으로 이렇게 생각하는 것은 몹시 안 좋은 습관이라 생각합니다.

모바일 어플리케이션과 웹 서비스의 경우 기계가 사용하는 것이 아닌 사람이 사용을 하기 때문이죠.

사람이 사용하기 좋게 만드는 것도 개발자가 생각해야 할 필수 고려 옵션이라 생각합니다.

일단 서론은 여기까지고…

제가 재직 중인 작은 회사에서 개발을 하며 고민했던 UX 관련 작업 경험을 공유해보고자 합니다.

물론 저도 전문적인 UI & UX 개발자는 아니지만, 순수하게 제가 고민하고 경험했던 부분을 기록하는 포스팅임을 감안해 주시길..


끔찍한 화면 구성, 그리고 개선

일단 제가 개발한 앱에 대한 간략한 설명을 하자면…

서버에서 Rest api를 호출한 결과 값을 화면에 보여주는 안드로이드 어플리케이션입니다.

먼저 초창기는 아니지만… 1차 버전의 메인 리스트를 보도록 하죠.

1차 2차

1차 화면을 보면..

RecyclerView를 통해 데이터를 보여주는데 일단 Cell(리사이클뷰의 아이템)의 UX는 잠시 접어두고…

상단이 너무 조잡하다는 것을 느낄 수 있습니다.

화면 상단의 네비게이션이라 할 수 있는 부분은 좌측 메뉴 옵션, 중간 타이틀, 우측 리프래시 버튼으로 구성이 되어 있고,
바 하단엔 뷰의 라벨 그리고 우측엔 리프래시 이후 경과시간을 보여주고 있습니다.

(설명까지 이렇게 복잡하다는 것은 확실히 문제가 있다고 할 수 있죠.)

게다가 구역 선택의 화면은 너무 동떨어진 느낌을 지울 수 없습니다.

약간의 개선을 통해 2차 화면이 만들어졌습니다.

하지만 색상과 네비게이션 바 타이틀, 그리고 좌측 메뉴가 뒤로가기로 변경된 부분 빼고는 크게 다른 점은 없네요?

1차버전에서는 사실 앱이 진입하고 나서 바로 저 화면으로 보여주면서 메뉴를 좌측 상단에 배치하는 구조였습니다.

하지만 이렇게 할 경우 사용자 입장에서는 메뉴라는 기능과 기계 현황이라는 항목이 같이 겹쳐 있어서 혼동이 올 수 있는 구조입니다.

또한 상단의 메뉴 중 리프래시 버튼 구조가 있는 것이 딱히 맘에 들지 않습니다.

최근 모바일 어플리케이션의 UX방법 중에는 저런 TableView(iOS) 또는 ListView & RecyclerView(Android) 뷰 컴포넌트를 상단에서 당겨서,
뷰 데이터를 갱신하는 구조를 많이 사용합니다.

그래서 최종적으로는 메뉴와 메인화면, 그리고 기계리스트 두 가지 화면으로 나눠서 구성하였습니다.

메인화면 메뉴 개선된 기계 현황

메인화면에서는 데이터를 나눠서 대시보드 스타일로 맞춰서 구현을 하였고, 메뉴 또한 리스트나 다른 메뉴에서의 접근이 아닌,
메인급 화면에서만 접근이 가능하도록 변경하였습니다.

그리고 개선된 기계 현황의 경우 리프래시를 Pull-down 스타일로 변경하고, 기계 리스트에서 보여주는 데이터의 정렬 부분을 추가함으로써,
데이터 표현에 대한 방법을 개선하였습니다.

이것은 여담이지만… 저 정렬을 스트림과 필터로 구현하려 하였지만…
안드로이드가 Java8을 완벽하게 지원하는 것이 아니기 때문에 일일히 나눠서 구현하게 되었습니다. (제발 Java8이 안드로이드에서도 완벽하게 지원되길…)


마지막으로는 저 기계 리스트에서 아이템을 선택했을 때 자세한 기계 정보 화면을 보여주는 부분을 개선한 것을 보도록 하죠.
구형 버전 개선 버전 개선 버전

구형 버전을 보면…

정말 답이 없습니다….

비 개발자분에게 시연하였을 때도 데이터가 명확하게 보이지 않을 뿐더러 화면이 뭔가 조잡하게 느껴진다는 반응이었습니다.

화면 상단의 버튼식으로 움직이는 부분 또한 문제가 되었습니다.

그래서 이 부분은 Android의 ViewPager와 SmartTab이라는 오픈소스를 활용하여 개선하였습니다.

또한 안의 데이터를 조각으로 나누고 Bootstrap 스타일로 화면을 구성하였습니다.

사실 다른 UI 방법이 있었겠지만, 디자이너가 없는 관계로 제 선에서는 저 방법이 최선이었습니다.(사실 지금 다른 아이디어도 떠오르긴 하네요.)


결론…UX 그리고 UI

일개 개발자인 제가 UX 그리고 UI를 한꺼번에 작업하긴 어려웠지만, 그래도 어느정도 깔끔하게(?) 데이터를 표현하는 방법까지는 도달할 수 있었습니다.

제일 중요한 것은 노력이라 생각합니다.

사실 값을 보여주는 것 자체에 집중을 한다면 결과물을 뽑아내는 것에는 아무런 문제가 되지 않을 것입니다. (개발자 입장에서는)

하지만 그 값을 어떻게 보여주고, 표현하는지가 그 결과물의 진수를 담는 것이라 생각이 듭니다.

저는 개발을 할 때 항상 최선을 다한다.

물론 알바 또는 프리랜서로 작업하는 결과물의 경우에는 가이드를 따라야 하기에 제약이 있지만..

제가 토이 프로젝트로 진행하는 경우에는 정말 다양한 것을 고려하고, 개발을 합니다..

항상 모든 작업은 장인정신을 가지고 만들어가자 라는 것이 제 개인 철학입니다.

이 철학을 잊지 말고, 오늘도 저의 결과물을 어떻게 더 개선할 지 좀 더 고민을 해봐야겠습니다..

잘 설계된 UX는 메뉴얼이 필요없고, 깔끔하고 심플하게 디자인된 UI는 사용자에게 편리함을 이끌어낸다.

개발일지(는 아니고 근황?)

무엇을 했길래 글 리스폰 상태가?

기술 블로그에 괜찮은 내용으로 포스팅을 많이 올리고 싶지만…

어제(24일)까지 프로젝트를 마무리 짓느라 제대로 글을 올릴 수 없었습니다.

서버와 안드로이드간의 개발을 모두 마무리 짓고, 어제 플레이스토어에 정상적으로 등록까지 마쳤습니다.

지금 재직 중인 회사는 서비스를 개발하는 회사는 아니고, 제조쪽 기계와 연관된 서비스(는 아니고 어플리케이션?)를(을) 개발하는 회사입니다.

사실 서비스 개발을 중심으로 하는 회사를 가고 싶었지만, 개발자로써의 자질을 좀 더 키운 후 이직하는 것을 목표로…

어제까지 개발을 마무리하고 문서화를 하느라 포스팅을 제대로 못하였지만,
이제 프로젝트가 마무리 되어서 진행하며 배우거나 찾은 것을 위주로 포스팅을 진행할 예정입니다.


주요 포스팅 예정

일단 포스팅 예정인 주제는 아래와 같습니다.

  • Android

    • Retrofit 통신 관련
    • 앱 UX 개선 작업
    • 개발하며 찾은 라이브러리 및 개발 방법
  • Server Side (Spring boot)

    • Fcm 토큰 설계
    • Jwt 관련

해당 포스팅을 검색하시려면 Tag기능을 활용하시면 금방 검색이 가능하고, 관련 내용 업데이트를 진행할 예정입니다.

안드로이드 로그

안드로이드 개발자라면 개발에서 결과 또는 테스트를 위해 로그 기능을 사용할 것입니다.

하지만 이 로그는 배포버전이 아닌 개발버전에서만 나오게끔 하는 것이 제일 베스트한 선택일 것입니다.

또한 기본적으로 사용하는 로그의 경우 아래와 같이 사용하게 됩니다.

1
Log.d("Log tag", "Log message...");

Log 클래스 뒤에 메서드는 로그 레벨로써 아래와 같이 분류됩니다.

로그 레벨 설명 우선순위
e(Error) 오류가 났을 때 사용 매우 높음
w(Warn) 오류는 아니지만 문제가 발생할 가능성이 있을 때 사용 높음
i(Info) 일반적으로 사용 중간
d(Debug) 개발 중 확인이 필요할 경우 사용 낮음
v(Verbose) 자세한 정보 등을 표시할 때 사용 매우 낮음

로그에 대해 좀 더 상세한 설명은 공식문서를 참고하시길

이제 기본적인 내용은 여기까지하고, 포스트에서 원하는 목표는 다음과 같습니다.

1. 개발버전에서만 동작하는 기능 만들기
2. 나만의 로그 만들기

그럼 하나씩 풀어나가보도록 하겠습니다.


1. 개발버전에서만 동작하는 기능 만들기

먼저 현재 어플리케이션이 구동되는 상태가 개발버전인지 아니면 배포버전인지 알아야 합니다.

저의 경우 아래와 같이 전역 메서드를 하나 생성하고, 호출하는 방식으로 구현하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SomeCls {

/**
* 현재 디버그 상태(개발모드) 체크를 하는 메서드
* @param context
* @return true-개발모드 / false-릴리즈 모드
*/
public static boolean isDebuggable(Context context) {
boolean debuggable = false;
PackageManager pm = context.getPackageManager();
try{
ApplicationInfo appinfo = pm.getApplicationInfo(context.getPackageName(), 0);
debuggable = (0 != (appinfo.flags & ApplicationInfo.FLAG_DEBUGGABLE));
}catch (PackageManager.NameNotFoundException e) {
Log.d("MyLog", makeClazzAndMethodName(context.getClass(), "isDebuggable") + " error msg = " + e.getMessage());
}
return debuggable;
}

}

위 메서드를 호출할 경우 boolean을 반환하는데 true일 경우 개발모드(Debug), false일 경우 배포모드입니다.

일단 개발버전을 체크하는 메서드는 준비되었습니다.

그럼 다음과 같이 로그를 개발버전에서만 보이게끔 처리할 수 있을 것입니다.

1
2
3
if(SomeCls.isDebuggable(context)) {
Log.d("DEV-LOG", "Hello~ World Log");
}

그런데 매번 이렇게 로그를 작성하려면 뭔가 번거롭습니다.

특히 저 if(SomeCls.isDebuggable(context)) 이 부분이 몹시 거슬립니다.

또한 Context 객체를 계속 전달을 받아야 하는 부분도 번거롭네요.

이것을 해결하기 위해선 다양한 방법이 있습니다.

  1. 어플리케이션에서 제일 먼저 진입하는 Activity에서 전역(정적) 변수를 선언하고, 이 전역(정적) 변수를 통해서 체크하는 방법.
  2. 전역에서 사용할 클래스를 하나 생성 후, 전역 변수를 하나 만들고,
    어플리케이션에서 제일 먼저 진입하는 Activity에서 앞서 설명한 전역 변수에 값을 저장 후 체크하는 방법.

말로 쓰면 어려우니 간단한 코드로 설명을 대신합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
///제일 처음 진입하는 Activity == MainActivity
///전역 클래스 == CommUtil 전역 변수 == boolean ISDEBUG

///Type 1 (MainActivity 안)

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ISDEBUG = isDebuggable(getApplicationContext());
}


///Type 2 (MainActivity 안)

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
CommUtil.ISDEBUG = isDebuggable(getApplicationContext());
}

이제 개발버전에서만 동작하는 기능을 구현하였습니다.

하지만 로그를 작성할 때마다 if문을 계속 같이 쓰면 번거롭습니다.

또한 항상 로그 태그와 함께 메세지를 넣는 부분도 거슬립니다. (물론 경우에 따라서 로그 태그를 통해 분기하기도 하지만…)

그렇다면 나만의 로그 클래스를 만들고 그 안에서 위 작업을 한꺼번에 처리한다면?


2. 나만의 로그 만들기

이제 나만의 로그 클래스를 만들어 보도록 하겠습니다.

프로젝트에서 적당한 로그 클래스를 만들어봅니다.

먼저 코드를 참고하기길..

```java
public class MyLog {
private static final String TAG = “MyAppLog”;

public static final void i(String msg) { if (ISDEBUG) { Log.i(TAG, makeLogBody(msg)); } }
public static final void v(String msg) { if (ISDEBUG) { Log.v(TAG, makeLogBody(msg)); } }
public static final void w(String msg) { if (ISDEBUG) { Log.w(TAG, makeLogBody(msg)); } }
public static final void d(String msg) { if (ISDEBUG) { Log.d(TAG, makeLogBody(msg)); } }
public static final void e(String msg) { if (ISDEBUG) { Log.e(TAG, makeLogBody(msg)); } }

/**
 * 로그에 띄울 메세지를 만들어주는 메서드
 * @param logMsg
 * @return
 */
private static String makeLogBody(String logMsg) {
    StackTraceElement ste = Thread.currentThread().getStackTrace()[4];
    StringBuilder sb = new StringBuilder();
    sb.append("[");
    sb.append(ste.getFileName().replace(".java", ""));
    sb.append("::");
    sb.append(ste.getMethodName());
    sb.append("] ");
    sb.append(logMsg);
    return sb.toString();
}

}


일단 사용방법은 **MyLog.** 으로 사용하면 됩니다.

내부에서 사용된 **ISDEBUG**의 경우 위에서 설명한 방법을 참고합니다.

**makeLogBody** 메서드의 경우 로그의 보여줄 메세지를 만들어주는 메서드로써 보여주고 싶은 내용은 개발자의 입맛에 맞게끔 바꾸면 됩니다.


<br>

# 결론

자신의 입맛에 맞는 커스텀 로그를 만드는 방법을 간단히 알아보았습니다.

오픈소스에서도 다양한 안드로이드용 로그 라이브러리가 있습니다.

전에 찾아본 것으론 **[Logger](https://github.com/orhanobut/logger)** 라는 것도 있었습니다.

처음 해보는 리뷰이자 늦은 리뷰?

||

지금으로부터 약 한달 전? 10월 27일에 아이폰 텐? (이하 X)의 사전 예약이 시작되었습니다.

그 후 11월 3일에 정식 출시를 하였고, 다양한 리뷰가 많이 풀리기 시작했습니다.

고가임에도 불구하고, 국내에서 많은 사람이 직구를 하여 구매한 것(지극히 개인적 생각…) 같습니다.

늦었지만 저도 오늘 아이폰 X를 수령하게 되었습니다.

그 여정과 하루정도 사용한 리뷰를 포스팅해보도록 하겠습니다.

기술 블로그라서 리뷰는 안쓰려 했지만..기록용으론 나쁘지 않을 것 같아서 리뷰 포스팅을 시작해봅니다.


사전예약과 지옥의 배송기간.

미국 시간으로 27일 0시였나? 한국 시로는 28일 오후 4시쯤이었을 겁니다.

저도 3시 50분부터 구매 준비를 하였고, 4시에 Apple 공홈으로 들어갔지만…

역시 사이트는 마비상태였습니다.

하지만 애플 공식 앱으로 주문에 성공한 사람들이 있었고, 그들은 11월 3일 수령자로 선택이 되었지요.

11분에 결제까지 성공한 저는 11월 17~24일 배송으로 잡혔습니다.

결제는 11월 6일에 이뤄졌고, 배송은 11월 8일? 그쯤부터 시작되었습니다.

배대지까지는 저 정도 걸렸고, 한국에는 17일에 입항하였습니다. (진심 장저우에서 숙성기간만 아니었으면 일찍 받는건데…)

인천세관 우체국 배송

인천공항에서는 주말껴서 처리가 되었고, 우체국에서 오늘 배송을 해주었습니다. (정확히는 제가 기사님을 찾아갔습니… -_-;;)


안녕? iPhone X

이제 수령기를 써볼까 합니다.

블로그 용량 상 큰 이미지는 못 올려서 사이즈를 줄여서 올렸습니다. (양해를…)

일단 박스는 저렇게 생겼습니다.

실버는 박스가 저 색상인데, 스페이스 그레이는 박스 색상이 다르다고 합니다.

일단 전 쌩폰을 좋아하지만…유리 재질이라 떨구면 아작날거 같아서 케이스만 하나 씌웠습니다.

하지만 보호필름 따윈 붙이지 않았습니다. (Simple is the best)

아래는 전면 사진입니다.

원본 원본

사진으론 표현이 다 안되지만…상당히 깔끔합니다.

개인적으로 LCD보단 OLED가 더 보기 좋은 것 같습니다.

원본 원본

아이폰 X가 공개되고 캘리포니아 인피니티 루프에서 실제 기기가 오픈되었을 때 실버 모델을 보고 이거다 싶었던 것이 바로 위 사진입니다.

스그는 그냥 깜댕인데 얘는 아주 잘 빠진 모습입니다. (물론 개인적 취향)

디자인은 678 시리즈처럼 밋밋한 디자인이 아닌 매끈한 모습이네ㅇㅇ.

옆선은 다음의 사진을 참고하시길…

원본 원본

측면의 마감도 아주 이쁩니다.

근데 무게감이 생각보다 좀 있습니다.

전에 7+ 쓸때는 그냥 가볍다? 이런 느낌인데 이번 X는 뭔가 묵직함이 느껴집니다.

물론 이 부분도 사람마다 편차가 있겠지만 개인적으론 묵직한 느낌이 들었습니다.

다음은 페이스 아이디로 진입하는 영상입니다.
(어떻게 해야 할지 몰라서 tumblr에 영상 올리고 video 테그로 넣었습니다.)

트루뎁스 카메라랑 얼굴에 점을 뿌리는 빛은 사용자인 제 눈에는 보이지 않지만, 촬영할 때는 저렇게 붉은 빛으로 보이네요.

마지막으로 페이스 아이디와 앱 화면을 보도록 하겠습니다.

설정의 페이스 아이디 네이버 앱의 메인
||

네이버 앱은 아직 X의 화면에 최적화를 못했지만, 카페앱은 어느정도 잘 되어있습니다.

하지만 상단의 엘프 귀쪽의 버튼은 터치가 어렵습니다.


결론

음…

하루정도 써본 사용 후기를 요약해보면 다음과 같습니다.

  1. 일단 생각보다 묵직함.
  2. M 탈모는 생각보다 안 거슬림. (개인 취향차 인듯)
  3. 실물은 정말 이쁨. (하지만 가격의 상태가?)
  4. Face Id는 정말 터치보다 편함. (쓰기 전까진 뭔 차인지 모름)
  5. 일단 빠릿빠릿 함.
  6. X 해상도 대응하려면 정말 거지같을 것 같다는 생각. (오토레이아웃으로도 다 되진 않는듯..)
  7. 근데 돈 없으면 무리해서 살 필요는 없는 제품인듯?

이 정도일듯 합니다?

그리고 이건…그냥 제 개인적 생각인데..
내가 정보수집을 위해 잠깐씩 가는 카페가 있는데 여기가 좀 광신적인 사람들이 많습니다;; (그래서 바로 탈퇴 ㅋ)
사실 휴대폰이라는게 소모품 중 하나고 쓰다 액정이 나가거나 사용 부주의로 깨질 수도 있고,
무튼 이런 소모적인 것이 발생하면 그냥 교체하면 그만이라 생각합니만…
근데 저기서는 무슨 휴대폰에 금치장을 하듯 아주 신주단지 모시듯 하는데…
소모품이고 고장나면 교체하면 그만인데 겨우겨우 모아 무리해서 산 것 처럼?
걍 개인적으론 보기 많이 많이 안좋네요.
그냥 형편에 맞게 필요한 제품을 사는게 좋은 것 같단 생각이 듭니다.

PS : 역시..리뷰는 힘든거 같습니다.
이거 쓸 시간에 내일은 기술적인 포스팅 정리해둔 것을 올려야겠습니다..

내일은 꼭 알찬 내용으로 포스팅을…

안드로이드에서 Font Awesome을?

웹 개발을 할 때 아이콘 같은 것을 표시하기 위해서 Font Awesome을 많이 사용했습니다.

이것을 안드로이드에서도 사용할 수 있는데 어떻게 쓰는지 알아보도록 하겠습니다.


Ready?

실습 환경은 다음과 같습니다.

  • OS : Mac OS Sierra
  • Android Studio 2.3.3

일단 안드로이드 프로젝트 생성 및 기타 설정은 생략하도록 하겠습니다.

기존의 프로젝트 또는 예제를 하나 생성하여 시작한다는 가정하에 진행하도록 하겠습니다.

또한 Text View에 아이콘을 올려보는 것을 목표로 합니다.


How to use?

Font Awesome의 아이콘 중 Bell(위 이미지)을 구현해보는 것을 목표로 하겠습니다.

  1. Font Awesome 사이트에서 다운로드 후 압축을 풀고 fonts라는 디렉토리에서 fontawesome-webfont.ttf 파일을 확인합니다.
  2. Font Awesome 사이트에서 Bell을 확인할 때 아래 사진에서 Unicode 부분의 값을 확인해두도록 합니다. (여기선 f0c9)
  3. 스튜디오 프로젝트에 assets 디렉토리에 fontawesome-webfont.ttf파일을 추가해줍니다.
  4. res 디렉토리에서 strings.xml에 값을 추가해줍니다..
    예제 : <string name="fa_bell">&#xf0c9;</string>
    유니코드 앞에 &#x를 붙이고, 끝에는 세미콜론(;)을 붙여줍니다.
  5. 그 후 XML에서 TextView를 추가하고 속성에 android:text="@string/fa_bell"을 추가해줍니다.
  6. 구현할 Activity에서 Typeface를 추가해주도록 합니다.
    예제 : Typeface fontAwsome = Typeface.createFromAsset(getAssets(), "fontawesome-webfont.ttf");
    Typeface.createFromAsset의 두 번째 인자는 1번에서 추가한 파일명을 넣어줍니다.
  7. 구현할 Activity에서 5번의 TextView의 속성에 setTypeface() 속성을 추가해줍니다.
    예제 : someTv.setTypeface(fontAwsome);

위와 같은 과정을 마치고 앱을 구동해보면 정상적으로 나올 것입니다.