당신의 게임은 안전한가요?

시큐어 코딩과 관련해서 경험한 이야기가 있습니다.
잡지에 있는 하나의 글처럼 가볍게 읽어주시면 감사하겠습니다.

취미로 아주 작은 게임 서버를 운영하는 친구가 있습니다.
친구는 자신이 만든 게임을 플레이해보라고 제안했습니다.
그런데 저는 친구의 의도대로 순순히 게임을 즐길 생각이 없었습니다.

사건의 시작

단기간에 강해지길 원했습니다.
노가다없이 게임 내 재화를 많이 얻고 싶었고, 가장 초보적인 방법인 메모리 변조를 해보기로 했습니다.
이 공격을 위해 만들어진 도구인 치트엔진을 사용해서 첫 공격을 시도했습니다.

첫 번째 스캔

골드를 늘리기 위해 현재 가진 골드(71497)를 입력하고 스캔했습니다.
그런데 동일한 값을 가진 항목이 여러 개 검색되어 어떤 게 골드인지 특정할 수 없었습니다.
골드를 의미하는 주소를 정확히 찾기 위해 골드를 2만큼 더 얻어서 71499로 만든 후 다시 스캔해봤습니다.

두 번째 스캔

이것을 반복하다가 결국 하나가 남게 되면 정확한 값을 찾는 데 성공한 것입니다.
찾은 값은 마음대로 변경할 수 있었고, 변경된 값이 게임 내에도 잘 반영되었습니다.

이 방법으로 스탯, 경험치, 아이템, 골드 등 모든 것을 복사하거나 얻었고, 이것을 알게 된 친구의 표정은 어두워졌습니다.

어떻게 이런 게 가능했을까요?
친구가 만든 게임 서버는 클라이언트를 100% 신뢰하고 있었고, 대부분의 로직이 클라이언트에 있었습니다.
그리고 클라이언트가 보낸 데이터를 아무런 의심 없이 게임 서버 DB에 저장해버렸죠.

게임에 필요한 모든 것을 얻었지만 여기서 멈추지 않았습니다.
값 하나를 바꾸기 위해 매번 이 작업을 하기에는 너무나 번거로웠습니다.
어떻게 하면 더 쉽게 재화를 얻을 수 있을까 고민하다가 서버와 클라이언트 사이에 오가는 값을 조작해보기로 했습니다.

본격적인 조작

값을 조작하기에 앞서 어떤 패킷들이 오가는지 확인하고 싶었습니다.
패킷 분석 도구로 와이어샤크라는 유명한 도구가 있습니다.
이 도구로 모니터링을 시작한 채 게임에 접속하자마자 패킷이 쏟아지기 시작했습니다.
사냥터에 들어가 보니 이런 내용을 가진 패킷도 보입니다.

1
2
<mon_move>177,6,4,6,11</mon_move>
<!-- 대략 몬스터가 움직인다는 뜻 -->

패킷은 전혀 암호화되어있지 않았고, 그때 저는 이런 생각이 들었습니다.

서버로 패킷을 바꿔 보내면 데이터 조작이 가능하겠구나!

바로 실행에 옮겼습니다. 게임에 로그인된 상태이니 바로 데이터를 전송해보기로 했습니다.
1
2
3
4
5
ip: 192.168.0.1
port: 12501
content: <str>9000</str>
<!-- IP가 192.168.0.1이고, 포트 번호가 12501인 곳에 힘 스텟 9000을 보냄 -->
<!-- 실제 패킷은 완전히 다르게 생겼습니다. -->

와이어샤크에서 패킷을 추출한 후 일반적인 에디터로 위와 같이 변경했고, bittwist를 사용해서 패킷을 전송해봤습니다.

1
2
3
4
5
6
7
$ bittwist -v -i eth0 test.pcap
sending packets through eth0
trace file: test.pcap

2 packets (2218 bytes) sent
Elapsed time = 0.021880 seconds
# 서버로 패킷을 보내는 데에 성공

패킷이 성공적으로 전송되었습니다.

패킷 전송 별거 아니군!

많은 기대를 하면서 게임에 접속해봤습니다.

???? 실패!!

하지만…기대와는 다르게 전혀 바뀐 게 없었습니다.

실패의 연속

왜 그랬을까? 한참 생각해보다가 이건 HTTP가 아니라 TCP 소켓 연결이라는 것을 알았습니다.
게임에 처음 접속하면 연결을 맺고, 게임을 하는 동안에는 그 연결을 유지한 채로 데이터를 주고받았습니다.
이미 클라이언트와 서버가 서로 연결되어있을 때, 그 중간에 패킷을 넣어 보내는 것은 어렵습니다.

아! 그럼 둘 다 나랑 연결하고, 데이터를 조작해서 전송하면 되겠구나!

새로운 도전을 하기로 했습니다. 이 방법은 중간자 공격이라고도 불립니다.
클라이언트와 서버 사이에 프록시 서버를 두고, 클라이언트와 서버가 통신할 때 이를 조작해서 전송할 수 있습니다.

이 작업을 쉽게 할 수 있는 도구로 Burp Suite가 있으며 이 도구는 HTTP 통신 시 값을 가로채서 조작하는 것에 자주 사용됩니다.
이것만으로는 TCP 소켓 통신을 가로챌 수 없어서 NoPE Proxy확장 프로그램도 함께 설치했습니다.
여러 글을 참고해서 이것저것 설정하고 나서 패킷이 잘 들어오나 확인해보았지만, 전혀 반응이 없었습니다.

패킷이 하나도 없어..ㅠㅠ

관련된 글을 더 찾아보니 NoPE Proxy는 DNS를 이용하는 방식이라서 서버에 도메인이 있어야 합니다.
친구의 서버는 IP만 있고, 도메인이 없어서 이 도구를 사용하는 데에 어려움이 있었습니다.
여기에 더 시간을 쏟는 것보다 다른 방법을 찾는 게 빠를 것 같아서 한참을 만지작대다가 포기했습니다.

새로운 마음가짐

적절한 도구가 더 없을까 찾아보다가 결국 직접 만들기로 했습니다. (저는 개발자니까요!)

게임 클라이언트인 것처럼 사기를 치고 연결해보자!

학창 시절에 TCP 소켓으로 채팅 프로그램을 만들었던 기억을 떠올려서 차근차근 만들었고, 직접 서버에 연결해봤습니다.

1
2
$ ./client 192.168.0.1 12501 # 예시 IP, port 입니다.
Connection Success

서버와 연결하는 데에 성공했습니다!
이제 로그인을 하고 나서 스탯 조작을 위한 명령어를 서버에 보내면 됩니다.

1
2
3
client: <str>9000</str>
server: <str>success</str>
<!-- 대략 힘 스탯을 9000으로 만들었다는 뜻 -->

이전에 와이어샤크로 수집한 패킷을 똑같이 보내보니 서버에서 정상적으로 응답이 왔습니다.
다른 스탯도 모두 9000으로 만들어서 보내봤습니다.

성공적

성공적입니다!! 이제 게임의 모든 걸 조작할 수 있습니다.
보스를 한 방에 없앨 수도 있고, 운영자 전용 아이템을 얻을 수도 있습니다.

친구는 이 상황을 실시간으로 보고 있었습니다.
입에 거품을 물고 있습니다.

여기서 멈출 수 없지

호기심은 여기서 끝나지 않았습니다. 다른 유저의 데이터도 변경해보고 싶었습니다.
앞에서는 데이터 조작을 간단하게 소개했지만, 사실 아래와 같은 순서로 이루어집니다.

서버 연결 > 로그인 > 데이터 로드 > 조작

일반적으로 A 유저로 로그인을 하면 A 유저의 데이터만 가져올 수 있는데,
A 유저로 로그인하고 나서, B 유저의 데이터를 가져오는 게 가능할지 궁금했습니다.

1
2
3
4
5
6
client: <login>userA</login>
server: <login>success</login>
<!-- A 유저로 로그인하고 나서 B 유저의 데이터 로딩 시도 -->
client: <dataload>userB</dataload>
server: <dataload>success</dataload>
<!-- B 유저 로딩 성공 -->

이게 되네!?

이제 저는 다른 유저의 데이터도 조작할 수 있습니다.
마음에 드는 유저에게 아이템을 주거나 저를 방해하는 유저의 모든 스탯을 1로 만들어버릴 수도 있습니다.

요약

  1. 친구가 아주 작은 게임 서버를 운영한다.
  2. 필자는 강해지고 싶었다.
  3. 필자는 클라이언트 메모리 변조로 모든 것을 얻었다.
  4. 친구의 표정은 어두워졌다.
  5. 클라이언트와 서버 사이의 패킷을 훔쳐보고 위조했다.
  6. 친구는 입에 거품을 물었다.
  7. 다른 유저 데이터까지 변경해버렸다.
  8. 친구는 쓰러졌다.

이를 통해 얻은 것

  1. 서버는 클라이언트가 보낸 정보를 무조건 신뢰하면 안 된다.
  2. 중요한 정보는 암호화하여 위변조가 어렵게 해야 한다.

Javascript Array 가공 map(), filter(), find(), reduce()

map()

배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환합니다.

1
2
3
4
5
6
7
8
9
10
11
const array1 = [1, 4, 9, 16];
const array2 = [{ id: 1, value: 'a'}, { id: 2}, { id: 3, value: 'b'}];

const map1 = array1.map(x => x * 2);
console.log(map1);
// expected output: Array [2, 8, 18, 32]

const map2 = array2.map(item => item.value);
console.log(map2);
// expected output: Array ['a', undefined, 'b']

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/map

filter()

주어진 함수의 테스트를 통과하는 모든 요소를 모아 새로운 배열로 반환합니다.

1
2
3
4
5
const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];

const result = words.filter(word => word.length > 6);
console.log(result);
// expected output: Array ["exuberant", "destruction", "present"]

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

find()

메서드는 주어진 판별 함수를 만족하는 첫 번째 요소을 반환합니다. 그런 요소가 없다면 undefined를 반환합니다.

1
2
3
4
5
const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];

const result = words.find(word => word.length > 6);
console.log(result);
// expected output: "exuberant"

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/find

reduce()

배열의 각 요소에 대해 주어진 리듀서(reducer) 함수를 실행하고, 하나의 결과값을 반환합니다

1
2
3
4
5
6
7
8
9
10
11
const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce

Javascript forEach()의 업그레이드 every(), some()

every()

배열 안의 모든 요소가 주어진 판별 함수를 통과하는지 테스트합니다.

빈 배열에서 호출하면 무조건 true를 반환합니다.

중간에 통과하지 않는 요소가 있으면( return false인 경우) 더이상 반복하지 않고 종료됩니다.

1
2
3
4
5
6
7
8
9
10
11
const array1 = [1, 30, 39, 29, 10, 13];
const array2 = [1, 30, 41, 29, 10, 13];

const result1 = array1.every((currentValue) => currentValue < 40);
console.log(result1);
// expected output: true

const result2 = array2.every((currentValue) => currentValue < 40);
console.log(result2);
// expected output: false

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/every

some()

배열 안의 어떤 요소라도 주어진 판별 함수를 통과하는지 테스트합니다.

빈 배열에서 호출하면 무조건 false를 반환합니다.

중간에 어떤 요소라도 통과하면( return true인 경우) 더이상 반복하지 않고 종료됩니다.

1
2
3
4
5
6
7
8
9
10
11
const array1 = [1, 2, 3, 4, 5];
const array2 = [1, 5, 3, 3, 7];

const result1 = array1.some((element) => element % 2 === 0);
console.log(result1);
// expected output: true

const result2 = array2.some((element) => element % 2 === 0);
console.log(result2);
// expected output: false

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/some

파티셔닝

파티셔닝

개념

테이블을 파티션이라는 작은 단위로 나누어 관리하는 기법

종류

  1. 수평 파티셔닝

    수평 파티셔닝

    • 개념
      • 샤딩과 동일한 개념
      • 스키마를 복제한 후 샤드키를 기준으로 데이터를 나누는 것을 말한다.
      • 즉, 스키마가 같은 데이터를 두 개 이상의 테이블에 나누어 저장하는 것을 말한다.
    • 장점
      • 데이터의 개수를 기준으로 나누기 때문에 데이터의 개수가 작아지고, 따라서 인덱스의 개수도 작아진다. 그러므로 자연스럽게 성능은 향상된다.
    • 단점
      • 데이터를 찾는 과정이 기존보다 복잡하기 때문에 latency가 증가한다.
      • 하나의 서버가 고장나게 되면 데이터의 무결성이 깨질 수 있다.
  2. 수직 파티셔닝

    수직 파티셔닝

    • 개념
      • 테이블의 컬럼을 기준으로 나누는 것을 말한다.
      • 정규화하는 것과 비슷하지만 이미 정규화된 데이터를 분리하는 과정이다.
    • 장점
      • 자주 사용하는 컬럼을 분리하여 성능을 향상할 수 있다.

분할 기준

  1. 범위 분할(Range Partitioning)
    • 연속적인 숫자나 날짜를 기준으로 분할한다.
    • 우편번호, 일별, 월별, 분기별 등의 데이터에 적합하다.
  2. 목록 분할(List Partitioning)
    • 특정 파티션에 저장될 데이터에 대한 명시적 제어가 가능하다.
    • 분포도가 비슷하며, 많은 SQL에서 해당 컬럽의 조건이 많이 들어오는 경우 유용하다.
    • [한국, 일본, 중국 -> 아시아][노르웨이, 스웨덴, 핀란드 -> 북유럽] 등의 예시가 존재한다.
  3. 해시 분할(Hash Partitioning)
    • 해시 함수의 값에 따라 어떤 파티션에 포함할지 여부를 결정한다.
    • 파티션을 위한 범위가 없는 데이터에 적합하다.
  4. 합성 분할(Composite Partitioning)
    • 위의 기술을 결합하여 사용하는 방법을 말한다.
    • 범위 분할을 할 수 있지만, 분할된 결과가 너무 커서 효과적으로 관리할 수 없는 경우에 유용하다.
    • 범위-목록, 범위-해시 등의 예시가 존재한다.

참조

Test에서 Fixture란?

정의

테스트를 수행하는 데 필요한 정보나 오브젝트

설명

일반적으로 픽스처는 여러 테스트에서 반복적으로 사용되기 때문에 @Before메소드를 이용해 생성해두면 편리하다.

예시

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserDaoTest {
private UserDao dao; //Fixture
private User user1; //Fixture
private User user2; //Fixture

@Before
public void setUp() {
...
this.user1 = new User("rookie", "루키맨", "spring1장");
this.user2 = new User("rokie", "로키맨", "spring2장");
}
...
}

Redis 설치

Redis 서버

Redis 설치 및 실행

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
[~]$ wget http://download.redis.io/redis-stable.tar.gz
[~]$ tar xvzf redis-stable.tar.gz
[~]$ cd redis-stable/src
[~]$ vim Makefile

PREFIX?=~/apps/redis-server <- 이부분 수정

[~ src]$ mkdir ~/apps/redis-server
[~ src]$ make PREFIX=~/apps/
[~ src]$ make test
[~ src]$ make install
[~ src]$ cd ~/apps/redis-server
[~ redis-server]$ mkdir conf
[~ redis-server]$ cp ~/redis-stable/redis.conf ./conf/
[~ redis-server]$ vim conf/redis.conf

bind x.x.x.x <- 이 부분을 현재 서버 IP로 설정
daemonize yes <- 이 부분 no를 yes로 수정

[~ redis-server]$ ./bin/redis-server ./conf/redis.conf
[~ redis-server]$ ps -ef | grep redis

- 0000 1 0 00:00 ? 00:00:00 ./bin/redis-server conf/redis.conf x.x.x.x:6379
- 0000 00000 0 00:00 pts/0 00:00:00 grep redis

실행 스크립트

1
2
3
4
5
6
#!/bin/bash
REDIS_DIR=~/apps/redis-server

#### Main #####
# apps/redis-server/bin/redis-server apps/redis-server/conf/redis.conf
$REDIS_DIR/bin/redis-server $REDIS_DIR/conf/redis.conf

Spring

pom.xml

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>biz.paluch.redis</groupId>
<artifactId>lettuce</artifactId>
<version>4.5.0.Final</version>
</dependency>

application.properties

1
2
3
4
5
spring.redis.lettuce.pool.max-active=10
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.min-idle=2
spring.redis.port=6379
spring.redis.host=x.x.x.x

RedisConfiquration.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration    
public class RedisConfiguration {

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory("x.x.x.x", 6379);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());

return redisTemplate;
}
}

RedisServiceTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisServiceTest {

@Autowired
RedisTemplate<String, Object> redisTemplate;

@Test
public void test() {
ValueOperations<String, Object> vop = redisTemplate.opsForValue();

vop.set("test1", "햇님");
assertEquals("햇님",vop.get("test1"));
}
}

The Context Container

원문

https://tomcat.apache.org/tomcat-8.5-doc/config/context.html#Context_Parameters

소개

기본 디렉토리로 $CATALINA_BASE 의 상대 경로를 사용하고, 설정하지 않은 경우에는 $CATALINA_HOME으로 설정됩니다. 컨텍스트 경로는 가장 긴 Request URI 부터 매칭됩니다.

속성

  • allowCasualMultipartParsing
    대상 서블릿이 @MultipartConfig 어노테이션으로 표시되지 않은 경우에도 HttpServletRequest.getPart* 또는 HttpServletRequest.getParameter*가 호출 될 때, 톰캣이 multipart / form-data 요청 본문을 자동으로 구문 분석해야하는 경우 true로 설정합니다. false가 아닌 다른 설정은 톰캣이 기술적으로 규격을 준수하지 않는 방식으로 작동하도록합니다. 기본값은 false입니다.

  • allowMultipleLeadingForwardSlashInPath

    톰캣에 여러 /를 단일 /로 정규화합니다. 이것은 URI가 파일 시스템 경로로 변환되는 것처럼 파일 시스템의 동작과 일관성을 유지하지 위함입니다. 기본값은 false이며 여러 개의 /문자를 접을 수 있습니다.

  • altDDName
    alternative deployment descriptor의 절대 경로입니다. 이는 /WEB-INF/web.xml에 있는 default deployment descriptoroverride 합니다.

  • backgroundProcessorDelay

    backgroundProcess메소드를 호출할 때 까지의 지연시간을 초 단위로 나타냅니다. 기본값은 -1이고, 컨텍스트가 상위 호스트의 백그라운드 처리 스레드에 의존함을 의미합니다.

  • className

    사용할 Java클래스 이름. 이 클래스는 org.apache.catalina.Context인터페이스를 구현해야합니다. 지정하지 않으면 표준값이 사용됩니다.

  • containerSciFilter

    SCI가 제공한 컨테이너의 정규식으로 이 컨텍스트에 사용하지 않도록 필터링합니다. 매칭은 java.util.regex.Matcher.find()를 사용하므로 정규 표현식은 필터링을 위해 SCI가 제공 한 컨테이너의 정규화 된 클래스 이름의 하위 문자열과 일치해야합니다. 지정하지 않으면 필터링이 적용되지 않습니다.

  • cookies

    브라우저가 쿠키를 지원해야하고, 세션 식별 통신에 쿠키를 사용하려면 true로 설정합니다.

  • crossContext

    이 응용 프로그램 내에서 ServletContext.getContext() 호출이 이 가상 호스트에서 실행중인 다른 웹 응용 프로그램의 요청 디스패처를 성공적으로 반환하도록하려면 true로 설정합니다. 보안이 있는 환경에서는 false (기본값)로 설정하여 getContext()가 항상 null을 반환하도록합니다.

  • docBase

    웹애플리케이션을 위한 문서 디렉토리나 웹애플리케이션의 아카이브 파일의 경로이름입니다. 절대경로나 appBase의 상대경로로 지정이 가능합니다. 컨텍스트 요소가 server.xml에 정의되어 있거나 docBase가 호스트의 appBase아래에 정의되어 있지 않다면 필드의 값을 설정하지 않아야 합니다. docBase에 심볼릭 링크가 사용되면 톰캣을 재시작하거나 컨텍스트를 재배포해야 적용됩니다.

  • dispatchersUseEncodedPaths

    인코딩 여부를 제어합니다. 이것은 톰캣이 요청 디스패처를 얻기 위해 호출을 처리하는 것과 내부적으로 요청 디스패처를 얻는 데 사용되는 경로를 생성하는 것에 모두 영향을 줍니다. 기본값은 true입니다.

  • failCtxIfServletStartFails

    서블릿이 load-on-startup >=0인 경우 컨텍스트 시작을 실패하도록 하려면 true로 설정합니다. 지정하지 않으면 상위 호스트의 구성에서 동일한 이름의 속성이 지정된 경우 그것을 사용힙니다. 그렇지 않으면 기본값이 false가 사용됩니다.

  • fireRequestListenersOnForwards

    톰캣이 요청을 전달할 때 구성된 ServletRequestListeners를 시작하려면 true로 설정합니다. 주로 ServletRequestListeners를 사용하여 요청에 필요한 환경을 구성하는 CDI 프레임 워크 사용자에게 유용합니다. 지정하지 않으면 false의 기본값이 사용됩니다.

  • logEffectiveWebXml

    애플리케이션이 시작될 때 웹애플리케이션에 사용되는 유효한 web.xml이 (INFO 수준에서) 기록되도록하려면 true로 설정합니다. 효과적인 web.xml은 톰캣이 구성한 모든 기본값과 발견 된 web-fragment.xml 파일 및 주석을 결합한 애플리케이션의 web.xml입니다. 지정하지 않으면 false의 기본값이 사용됩니다.

  • mapperContextRootRedirectEnabled

    활성화 된 경우 웹애플리케이션 컨텍스트 루트에 대한 요청은 기본 서블릿이 아닌 매퍼가 필요에 따라 리디렉션됩니다. 이것은 효율적이지만 컨텍스트 경로가 있는지 확인하는 문제가 있습니다. 지정하지 않으면 기본값 true가 사용됩니다.

  • mapperDirectoryRedirectEnabled

    활성화 된 경우 웹애플리케이션 디렉토리에 대한 요청은 기본 서블릿이 아닌 매퍼가 필요에 따라 리디렉션됩니다. 이것은 효율적이지만 디렉토리가 있는지 확인하는 문제가 있습니다. 지정하지 않으면 기본값 false가 사용됩니다.

  • override

    전역 또는 호스트 기본 컨텍스트의 설정을 무시하려면 true로 설정합니다. 기본적으로 기본 컨텍스트의 설정이 사용되지만 컨텍스트에 대해 동일한 속성을 명시적으로 설정하여 무시할 수 있습니다.

  • path

    처리 할 적절한 웹애플리케이션을 선택하기 위해 각 request URI의 시작과 대조되는 이 웹 응용 프로그램의 컨텍스트 경로입니다. 특정 호스트 내의 모든 컨텍스트 경로는 고유해야합니다. 빈 문자열 ( “”)의 컨텍스트 경로를 지정하면이 호스트에 대한 기본 웹애플리케이션을 정의하며 다른 컨텍스트에 할당되지 않은 모든 요청을 처리합니다.

    이 속성은 server.xml에서 컨텍스트를 정적으로 정의 할 때만 사용해야합니다. 다른 모든 상황에서는 경로가 .xml 컨텍스트 파일 또는 docBase에 사용 된 파일 이름에서 유추됩니다.

    server.xml에서 컨텍스트를 정적으로 정의하는 경우에도 이 속성은 docBase가 호스트의 appBase 아래에 있거나 deployOnStartupautoDeploy가 모두 false가 아닌 경우에만 설정해야합니다. 이 규칙을 지키지 않으면 이중 배포가 발생할 수 있습니다.

  • preemptiveAuthentication

    true로 설정되고 사용자가 보안 제한 조건에 의해 보호되지 않는 자원에 대한 자격을 제시하면, 인증자가 선점인증을 지원하는 경우에 사용자의 자격 증명이 처리됩니다. 지정하지 않으면 기본값 인 false가 사용됩니다.

  • privileged

    이 컨텍스트가 관리자 서블릿과 같은 컨테이너 서블릿을 사용할 수 있게 하려면 true로 설정합니다. 특별한 속성의 사용은 컨텍스트의 parent 클래스 로더를 Shared 클래스 로더 대신에 Server 클래스 로더로 변경합니다. 기본 설치에서는 Common 클래스 로더가 Server 클래스 로더와 Shared 클래스 로더 모두에 사용됩니다.

  • reloadable

    파일에 변화가 있을 때 자동으로 로딩할지 설정합니다. 기본값은 false입니다.

  • resourceOnlyServlets

    리소스가 존재하는 쉼표로 구분된 서블릿 이름의 목록입니다.

  • sendRedirectBody

    true인 경우 리디렉션의 응답에 RFC2616에서 권장하는 리디렉션의 세부 정보가 포함됩니다. 응답 본문을 포함하면 압축 필터와 같은 일부 애플리케이션 구성 요소에 문제가 발생할 수 있으므로 기본적으로 사용하지 않도록 설정합니다.

  • sessionCookieDomain

    세션쿠키에 사용할 도메인입니다.

  • sessionCookieName

    세션쿠키에 사용하는 이름입니다. 설정한 경우 웹애플리케이션에서 설정한 이름을 무시합니다.

  • sessionCookiePath

    세션쿠키의 경로입니다. 설정한 경우 웹애플리케이션에서 설정한 경로를 무시합니다.

  • sessionCookiePathUsesTrailingSlash

    Internet Explorer, Safari, Edge 브라우저에서 RFC6265를 위반하여 /foobar의 요청으로 /foo 경로를 가진 컨텍스트에 대한 세션 쿠키를 보내는 문제가 있습니다. 이 위험을 줄이려면 true로 설정합니다. 그러면 톰캣은 세션 쿠키와 연결된 경로에 후행 슬래시를 추가합니다. 위의 예에서 쿠키 경로는 /foo/가 됩니다.

    /*에 매핑 된 서블릿이 없으면 문제가되지 않습니다. 이 속성을 비활성화하려면 false로 설정합니다. 기본값은 false입니다.

  • swallowAbortedUploads

    톰캣이 중단된 업로드에 대한 추가 요청 데이터를 읽지 않고 대신 클라이언트 연결을 중단해야하는 경우 true로 설정합니다. 추가 데이터를 읽지 않으면 요청 처리 스레드가 더 빠르게 해제됩니다.

  • swallowOutput

    true인 경우에 system.outsystem.err로 출력한 바이트가 웹애플리케이션의 logger로 리디렉션 됩니다. 기본값은 false입니다.

  • tldValidation

    true인 경우에 TLD파일은 컨텍스트 시작시에 XML유효성을 검사합니다. org.apache.catalina.STRICT_SERVLET_COMPLIANCEtrue인 경우 디폴트 값은 true이고, 그렇지 않은 경우 디폴트 값은 false가 됩니다. true인 경우 성능에 좋지 않은 영향이 있습니다.

  • useHttpOnly

    클라이언트의 스크립트가 세션 ID에 접근하지 못하도록 합니다. 기본값은 true입니다.

  • useRelativeRedirects

    HTTP 1.1 이상이고 javax.servlet.http.HttpServletResponse#sendRedirect(String)의 호출로 생성된 헤더가 절대경로나 상대경로로 리디렉션 할지 여부를 제어합니다. org.apache.catalina.STRICT_SERVLET_COMPLIANCEtrue인 경우 디폴트 값은 false이고, 그렇지 않은 경우 디폴트 값은 true가 됩니다.

  • validateClientProvidedNewSessionId

    클라이언트가 새 세션ID를 제공하면 이 속성은 해당 ID가 유효한지 여부를 제어합니다. 기본값은 true입니다.

  • wrapperClass

    org.apache.catalina.Wrapper의 자바 클래스 이름입니다. 지정하지 않으면 기본값이 사용됩니다.

  • xmlBlockExternal

    true인 경우에 web.xml, web-fragment.xml, *.tld, *.jspx, *.tagx and tagPlugins.xml 파일의 파싱에 외부 엔터티를 로드할 수 없습니다. 기본값은 true입니다.

  • xmlNamespaceAware

    true인 경우에 web.xml, web-fragment.xml파일의 파싱이 namespace-aware가 됩니다. org.apache.catalina.STRICT_SERVLET_COMPLIANCEtrue인 경우 디폴트 값은 true이고, 그렇지 않은 경우 디폴트 값은 false가 됩니다. true인 경우 xmlValidation옵션도 true가 됩니다.

  • xmlValidation

    true인 경우에 web.xml, web-fragment.xml파일의 유효성(문법)을 검사합니다. org.apache.catalina.STRICT_SERVLET_COMPLIANCEtrue인 경우 디폴트 값은 true이고, 그렇지 않은 경우 디폴트 값은 false가 됩니다. true인 경우 성능에 좋지 않은 영향이 있습니다.

싱글톤 주의하기

문제

아래의 코드를 우선 읽어보자.

해결 전 심각한 코드

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
@Controller
public class NoReadRecipientMailboxController {

@Autowired
private NoReadRecipientMailboxMapper recipientMailboxMapper;
private String user_id_session;
private String searchContent;
private List<RecipientEmail> recipientEmail;
private List<RecipientEmail> recipientStarEmail;
private List<RecipientEmail> recipientSearchEmail;

@GetMapping("/noreadreceivemailbox")
public String mailbox(Model model, HttpServletRequest req) {
user_id_session = (String)req.getSession().getAttribute("user_id");
recipientEmail = recipientMailboxMapper.selectList(user_id_session);
model.addAttribute("noreadrecipientEmail", recipientEmail);

recipientStarEmail = recipientMailboxMapper.selectStarList(user_id_session);
model.addAttribute("noreadrecipientStarEmail", recipientStarEmail);

recipientSearchEmail = recipientMailboxMapper.searchMailTitle(searchContent, user_id_session);
model.addAttribute("noreadrecipientSearchEmail", recipientSearchEmail);
this.searchContent = "";

return "mailbox/noreadreceivemailbox";
}
@PutMapping("/noreadreceivemailbox/searchMail/title/{searchContent}")
public ResponseEntity<?> searchMailTitle (@PathVariable String searchContent) {
this.searchContent = "%" + searchContent + "%";

return new ResponseEntity<>(true, HttpStatus.OK);
}
}

문제점 발견

코드를 잘 읽어보면 처음에는 어떤 문제가 있는지 잘 모르는 경우가 많다. 하지만 이 코드는 굉장한 문제점을 가지고 있고, 이제부터 이 문제점을 해결해나가고자 한다.

1
private List<RecipientEmail> recipientEmail;

이러한 방식으로 메소드 외부에 인스턴스 변수를 생성했을 때 스프링은 최초 동작 시에만 새롭게 생성하고, 이후에는 해당 변수를 그대로 반환한다. 그리고 싱글톤은 Thread-Safety하지 않다!
여러 사용자가 동시에 이 서비스를 이용하는 경우에 다른 사용자의 값을 반환받을 경우도 생기는 것이다. 이렇게 되면 다른 사용자의 메일을 볼 수 있을지도 모른다. 아니 실제로 그런 일이 발생한다.

그러므로 아래처럼 메소드 내부에 변수를 선언하는 것이 중요하다.

해결 후 코드

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
@Controller
public class NoReadRecipientMailboxController {

@Autowired
private NoReadRecipientMailboxMapper recipientMailboxMapper;

@GetMapping("/noreadreceivemailbox")
public String mailbox(Model model, HttpServletRequest req, String searchContent, String view) {
String user_id_session = (String)req.getSession().getAttribute("user_id");

if(searchContent != null) {
String Content = "%" + searchContent + "%";
List<RecipientEmail> recipientSearchEmail = recipientMailboxMapper.searchMailTitle(Content, user_id_session);
model.addAttribute("noreadrecipientSearchEmail", recipientSearchEmail);
}
else if(view != null && view.equals("star")){
List<RecipientEmail> recipientStarEmail = recipientMailboxMapper.selectStarList(user_id_session);
model.addAttribute("noreadrecipientStarEmail", recipientStarEmail);
}
else {
List<RecipientEmail> recipientEmail = recipientMailboxMapper.selectList(user_id_session);
model.addAttribute("noreadrecipientEmail", recipientEmail);
}

return "mailbox/noreadreceivemailbox";
}
}

NHN 1주차 - 입문교육

요약

설레는 마음으로 회사에 가서 교육 들으며 조별 활동을 진행했다.
조금은 힘들었지만 소중한 사람들을 얻을 수 있었기에 좋은 경험이었다.
꽃등심은 맛있었다. :)

입사 첫날.

아침이 밝았다. 오늘은 내가 NHN 신입으로 첫 출근을 하는 날이다. 나와 같이 입사하는 동기들이 궁금했다. 어떤 친구들일까? 내가 잘할 수 있을까? 머릿속에 궁금증이 가득한 채 문밖으로 첫 발걸음을 내디뎠다.
그렇게 40분 후, 회사에 도착해서 사원증을 받아 교육장으로 향했다.
“아, 내가 정말 붙은 게 맞나…?”
아직 실감이 나지 않았다.
교육장에 들어서자, 사람들이 많이 와있었다. 다들 서로 어색해 보였다. 조용히 빵을 먹는 사람. 조용히 스마트폰만 보는 사람. 모두 다른 행동을 하고 있었지만, 아마 ‘설렌다…’는 마음은 모두 같았으리라.
나도 김밥 하나를 들고 자리에 앉았다. 아침까지 챙겨주는 회사가 어디 있을까.

입사를 진심으로 축하합니다.

스크린에 글이 보였다. 나 정말 합격했나보다. 신기했다. 좋았다. 기뻤다.
옆 사람과 인사도 나누고, 조금 친해지려던 참이었는데 입문교육이 시작되었다.
오후 5시까지 이어진 입문교육…
하루종일 교육을 들으려니까 지치기 시작했지만, 이어서 조별 활동이 시작되었고 쉴 틈도 없이 밤 9시 반이 되어 첫날 일정이 끝났다. 첫날부터 다들 힘들었고, 집에 도착하니 밤 10시가 넘었다. 씻고 정리 좀 하다 보니 시간이 늦어서 바로 잠이 들었다.

입사 둘째 날.

하루종일 교육들었다.
덕분에 몸은 조금 피곤했지만, 우리 회사에 대해 더욱 잘 알 수 있는 계기가 되었고, 더욱 똑똑한 열정을 가질 수 있었다.

입사 셋째 날.

아침 교육을 듣고, 오후에는 강남 스탬프 투어를 했다.
우리 조는 협업 열정 끈기 소통 신뢰 를 바탕으로 열심히 스탬프 투어에 임했고, 큰 기대는 하지 않았지만 좋은 결과를 얻었다. 덕분에 조원들과 더욱 친해졌다.

입사 넷째 날.

조별로 맡은 서비스를 발표하고, 셋째 날에 했던 스탬프 투어와 점수를 합쳐서 시상식을 진행했다. 주말을 투자한 덕분인지 좋은 결과를 얻었고, 입문교육에서 많은 것을 배울 수 있어서 좋은 경험이었다. ^______________^

<algorithm>

sort

오름차순

sort(vector.begin(), vector.end());

내림차순

#include <functional>
sort(vector.begin(), vector.end(), greater<[Data Type]>());

reverse

std::reverse(vector.begin(), vector.end());

find

std::find(vector.begin(), vector.end(), item) != vector.end()

기본 사용법

1
2
3
4
if ( std::find(vector.begin(), vector.end(), item) != vector.end() )
do_this(); //찾았다면
else
do_that(); //못 찾았다면

응용 사용법

1
2
3
4
5
list<int>::iterator it = find(list.begin(), list.end(), item);
if(it == list.end())
do_this();
else
list.erase(it); //it가 가리키는 노드 삭제

나머지는 필요할 때마다 추가 예정