문제

![error]](/images/20200505/image.png)

위 화면 처럼 임시 비밀번호 발급 form을 만들어서 버튼엔 onclick, input box엔 enter key event를 달아서 ajax로 처리하였는데,

​클릭 이벤트로는 정상적으로 동작하였으나 enter key 의 경우 http status 415 Unsupported Media Type 에러가 발생하였다

1
2
3
4
5
6
7
8
9
10
There was an unexpected error (type=Unsupported Media Type, status=415).
Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported
org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:225)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:158)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:131)
at org.springframework.web.method.support.
HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)
...

보통 ajax 통신 시 415 에러가 발생 할 경우 ajax에서 설정한 content type과 contoroller에서 설정한 content type이 다를 경우 발생하는 데 이 경우엔 모두 json으로 설정해놓은 상태 하였는데도 불구하고 발생하여 매우 당황스러웠다

ajax

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

function resetPassword() {
const userName = document.querySelector("input[name=username]").value.trim();
const data = { username : userName };

$.ajax({
type:"POST"
, url: "/reset/password"
, data: JSON.stringify(data)
, dataType : "json"
, contentType: "application/json"
, cache: false
, success: function(data) {
let alert = document.querySelector(".mb-0");
alert.innerText = data;

document.querySelector(".alert.alert-success").style.display = "block";
}
});
}

Controller

1
2
3
4
5
6
7
8
9
10
@PostMapping(value="/reset/password", produces = "application/json")
@ResponseBody
public String processResetPasswordForm(@RequestBody User user) throws JsonProcessingException {
String tempPassword = userService.setTempPassWord(user);
String msg = "임시 비밀번호 : " + tempPassword;

System.out.println(msg);
ObjectMapper objMapper = getObjectMapperConfig();
return objMapper.writeValueAsString(msg);
}

해결

하여 헤매던 도중 enter key event를 지정하지 않아도 이벤트가 발생하여 똑같이 415 에러를 뱉어내는 것으로

확인돼서 적용한 thymeleaf나, bootstrap 등 어디선가 submit 을 보내고 있다고 판단하여 preventDefault() function으로 이벤트를 정지시키고 작성한 ajax를 실행시켰더니 정상적으로 동작하였다

form의 submit event에 preventDefault()를 추가

1
2
3
4
5
document.querySelector("#frm").addEventListener('submit', function(e) {
e.preventDefault();

resetPassword();
});

정상적으로 실행된 모습

![success]]](/images/20200505/image2.png)


전체 코드

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

<!DOCTYPE html>
<html lang="ko"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/config :: configFragment"></head>
<body>
<div class="container">
<div class="d-flex justify-content-center h-100">
<div class="card" style="height:auto;">
<div class="card-header">
<h3>Reset Password</h3>
</div>
<div class="card-body">
<form id="frm" th:action=@{/reset/password} method="post">
<div class="alert alert-success" role="alert" style="display:none;">
<h4 class="alert-heading">Well done!</h4>
<p>해당 비밀번호는 임시로 생성된 비밀번호 입니다. <br />로그인 후 꼭 비밀번호를 변경하여 주세요.</p>
<hr>
<p class="mb-0"></p>
</div>
<div class="alert alert-danger" role="alert" style="float:left; margin-right:1rem; display:none;" ></div>
<div class="input-group form-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-user"></i></span>
</div>
<input type="text" onkeyup="checkUserName()" class="form-control" name="username" placeholder="username">
</div>
<div class="form-group">
<input type="submit" onclick="resetPassword()" value="확인" class="btn float-right login_btn" disabled>
</div>
</form>
</div>
<div class="card-footer">
<div class="d-flex justify-content-center links">
Don't have an account?<a href="/login">Sign In</a>
</div>
</div>
</div>
</div>
</div>
</body>
<link th:href="@{/css/login.css}" rel="stylesheet" type="text/css">
<!--Fontawesome CDN-->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous">

<script th:inline="javascript">

document.querySelector("#frm").addEventListener('submit', function(e) {
e.preventDefault();

resetPassword();
});

function checkUserName() {
const data = document.querySelector("input[name=username]").value;

let isExist = true;
$.ajax({
type:"POST"
, url: "/reset/password/check"
, data: JSON.stringify(data)
, dataType : "json"
, contentType: "application/json"
, cache: false
, success: function(data) {
let alert = document.querySelector(".alert.alert-danger");

if (empty(data)) {
alert.innerText = "";
alert.style.display = "none";
isExist = true;
document.querySelector(".btn.float-right.login_btn").removeAttribute("disabled");
} else {
alert.innerText = data;
alert.style.display = "initial";
isExist = false;
document.querySelector(".btn.float-right.login_btn").setAttribute("disabled", "disabled");
}
}
});

return isExist;
}

document.querySelector("input[name=username]").addEventListener("keydown", key => {
if (key.keyCode == 13) {
resetPassword();
}
});

function resetPassword() {
const isExist = checkUserName();

if (!isExist) {
return;
}

const userName = document.querySelector("input[name=username]").value.trim();
const data = JSON.stringify({ username : userName });

$.ajax({
type:"POST"
, url: "/reset/password"
, data: data
, dataType : "json"
, contentType: "application/json"
, cache: false
, success: function(data) {
let alert = document.querySelector(".mb-0");
alert.innerText = data;

document.querySelector(".alert.alert-success").style.display = "block";
}
});
}
</script>
</html>

전체 프로젝트 코드


참고 사이트

댓글 공유

문제

https://programmers.co.kr/learn/courses/30/lessons/60057


코드

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
public int solution(String s) {
int stringLength = s.length();

StringBuilder result = new StringBuilder();
String origin = new String();
char[] tochar = s.toCharArray();
int min = Integer.MAX_VALUE;

for (int i = 1; i <= stringLength / 2; i++) {
int compression = 1;
result.setLength(0);
origin = new String(tochar, 0, i);

String nextWord = new String();
for (int j = i; j < stringLength; j+=i) {
if (j + i > stringLength) {
nextWord = s.substring(j, stringLength);
} else {
nextWord = new String(tochar, j , i);
}

if (origin.equals(nextWord)) {
++compression;
} else {
if (compression != 1) {
result.append(compression);
}

result.append(origin);

origin = nextWord.toString();
compression = 1;
}
}

if (compression != 1) {
result.append(compression);
}

result.append(origin);

if (min > result.length()) {
min = result.length();
}
}

int answer = min > stringLength ? stringLength : min;
return answer;

흐름

  1. 문자열을 반으로 나눠서 2개로 나눈 것 보다 짧을 수 없으므로 문자열 길이 / 2 한 값 만큼 반복

  2. 비교 할 문자열을 구함(origin)

    • 처음엔 1개 짜리 이후엔 2개, 3개, … 증가 할 수 있도록 i를 증가 시키면서 i 번째 까지 자름
  3. i 번째 문자 이후로 비교 할 문자를 구함

  4. 같으면 압축율을 증가시키고

  5. 다르면 압축율과 문자를 더해서 압축문자열(result)을 만듬

  6. 비교 할 문자를 구하면서 마지막 문자는 안 붙여지고 팅기므로 for 문 밖에서 마지막 문자를 붙임

  7. 이전에 구했던 압축문자열 길이와 비교해서 더 작은 길이를 저장함


결과

번호 속도
테스트 1 통과 (0.99ms, 52.4MB)
테스트 2 통과 (2.92ms, 49.9MB)
테스트 3 통과 (1.72ms, 52.7MB)
테스트 4 통과 (0.99ms, 52.1MB)
테스트 5 통과 (0.84ms, 50MB)
테스트 6 통과 (1.03ms, 52.4MB)
테스트 7 통과 (3.49ms, 50.4MB)
테스트 8 통과 (2.49ms, 50.3MB)
테스트 9 통과 (4.46ms, 52.6MB)
테스트 10 통과 (9.25ms, 52.4MB)
테스트 11 통과 (1.25ms, 50.2MB)
테스트 12 통과 (1.28ms, 52.3MB)
테스트 13 통과 (9.63ms, 51.8MB)
테스트 14 통과 (4.35ms, 52.2MB)
테스트 15 통과 (1.31ms, 52.1MB)
테스트 16 통과 (0.85ms, 50.2MB)
테스트 17 통과 (8.91ms, 52.6MB)
테스트 18 통과 (7.70ms, 52.6MB)
테스트 19 통과 (8.63ms, 54.9MB)
테스트 20 통과 (7.89ms, 52.6MB)
테스트 21 통과 (9.99ms, 52.7MB)
테스트 22 통과 (10.63ms, 52.7MB)
테스트 23 통과 (9.68ms, 52.5MB)
테스트 24 통과 (7.89ms, 54.4MB)
테스트 25 통과 (10.43ms, 52.9MB)
테스트 26 통과 (8.17ms, 52.5MB)
테스트 27 통과 (15.60ms, 52.6MB)
테스트 28 통과 (0.88ms, 50.4MB)
  • result에 문자열을 더하지 말고 그냥 문자열 길이를 더하는 방식으로 하면 더 시간을 줄일 수 있음

테스트 케이스

1
2
3
4
5
6
7
8
assertEquals(7, test.solution("aabbaccc"));
assertEquals(9, test.solution("ababcdcdababcdcd"));
assertEquals(8, test.solution("abcabcdede"));
assertEquals(14, test.solution("abcabcabcabcdededededede"));
assertEquals(17, test.solution("xababcdcdababcdcd"));
assertEquals(1, test.solution("a"));
assertEquals(2, test.solution("aaaaa"));
assertEquals(3, test.solution("aaaaaaaaaa"));

참고 사이트

댓글 공유

문제

https://programmers.co.kr/learn/courses/30/lessons/42583?language=java


코드

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
public int solution(int bridge_length, int weight, int[] truck_weights) {
LinkedList<Truck> queue = new LinkedList<Truck>();

for (int i = 0; i < truck_weights.length; i++) {
queue.offer(new Truck(bridge_length, truck_weights[i]));
}

int answer = 1;
int bridgeOnTruck = 0;
while (!queue.isEmpty()) {

int totalWeight = 0;
for (int i = 0; i < bridgeOnTruck; i++) {
totalWeight += queue.get(i).weight;
}

int nextWeight = 0;
if (queue.size() > bridgeOnTruck) {
nextWeight = queue.get(bridgeOnTruck).weight;
}

if (totalWeight + nextWeight <= weight) {
if (queue.size() > bridgeOnTruck) {
++bridgeOnTruck;
}
}

for (int i = 0; i < bridgeOnTruck; i++) {
Truck truck = queue.get(i);
truck.position = truck.position - 1;
}

if (queue.peek().position == 0) {
queue.poll();
--bridgeOnTruck;
}

++answer;
}

return answer;
}

class Truck {
public int position;
public int weight;

public Truck(int position, int weight) {
this.position = position;
this.weight = weight;
}
}

흐름

  1. 트럭 array를 queue에 모두 저장
  2. 트럭 모두 건널 때 까지 반복
  3. 한 번에 몇 개의 트럭까지 건널 수 있을 지 계산하기 위해 현재 다리 위에 있는 트럭의 개수 만큼 돌면서 트럭 무게를 더함
  4. 다음 번 트럭까지 다리에 올라 올 수 있는 지 확인하기 위해 큐의 마지막 index가 아니라면 queue에서 다음 번 트럭의 무게를 가져옴
  5. 현재 다리에 있는 트럭들의 무게 + 다음 트럭의 무게를 더한 값이 다리가 버틸 수 있는 무게 보다 가벼우면
  6. 큐의 마지막인지 체크해서 큐의 마지막이 아니라면 그 다음 트럭도 다리 위에 올라와야 하기 때문에 다리 위 트럭의 개수(truckAmount)를 증가 시킴
  7. 다리 위 트럭의 수 만큼 반복하면서 트럭을 앞으로 한칸 씩 움직임
  8. 첫 번째 트럭이 다리를 건넜다면 queue에서 제거하고 다리 위 트럭 수를 하나 감소 시킴
  9. 초를 증가 시킴

결과

번호 속도
테스트 1 통과 (3.63ms, 52.1MB)
테스트 2 통과 (14.16ms, 52.1MB)
테스트 3 통과 (1.65ms, 53.3MB)
테스트 4 통과 (41.49ms, 53.8MB)
테스트 5 통과 (70.64ms, 53.9MB)
테스트 6 통과 (53.31ms, 56.1MB)
테스트 7 통과 (3.09ms, 50MB)
테스트 8 통과 (1.97ms, 50.3MB)
테스트 9 통과 (10.68ms, 54.6MB)
테스트 10 통과 (2.20ms, 53MB)
테스트 11 통과 (1.10ms, 52.5MB)
테스트 12 통과 (2.35ms, 52.2MB)
테스트 13 통과 (4.50ms, 52.2MB)
테스트 14 통과 (1.26ms, 53MB)

테스트 케이스

1
2
3
assertEquals(8, test.solution(2, 10, new int[] {7,4,5,6}));
assertEquals(101, test.solution(100, 100, new int[] {10}));
assertEquals(110, test.solution(100, 100, new int[] {10,10,10,10,10,10,10,10,10,10}));

댓글 공유

https://programmers.co.kr/learn/courses/30/lessons/62048?language=java


코드

1
2
3
4
5
6
7
8
9
10
11
12
public long solution(long w,long h) {
long big = w > h ? w : h;
long small = w > h ? h : w;

while (small != 0) {
long r = big % small;
big = small;
small = r;
}

return (w * h) - (w + h - big);
}

흐름

  1. 최대 공약수를 구함

  2. 전체 사각형 수 (w * h) 에서 안 멀쩡한 사각형 수(w + h - 최대 공약수)를 뺌


최대 공약수를 사용하는 이유

아래 블로그에서 잘 설명 해주셨음


결과

번호 속도
테스트 1 통과 (0.76ms, 50.7MB)
테스트 2 통과 (0.81ms, 52.4MB)
테스트 3 통과 (0.75ms, 52.6MB)
테스트 4 통과 (0.70ms, 52.6MB)
테스트 5 통과 (0.82ms, 51.9MB)
테스트 6 통과 (0.80ms, 52.1MB)
테스트 7 통과 (0.85ms, 52.4MB)
테스트 8 통과 (0.86ms, 54.6MB)
테스트 9 통과 (0.72ms, 52.3MB)
테스트 10 통과 (0.73ms, 52.6MB)
테스트 11 통과 (0.74ms, 52.2MB)
테스트 12 통과 (0.58ms, 52.5MB)
테스트 13 통과 (0.51ms, 52.5MB)
테스트 14 통과 (0.60ms, 52.6MB)
테스트 15 통과 (0.53ms, 50.3MB)
테스트 16 통과 (0.61ms, 52.5MB)
테스트 17 통과 (0.61ms, 52.3MB)
테스트 18 통과 (0.80ms, 52.3MB)

테스트 케이스

1
2
3
4
5
assertEquals(80, test.solution(8, 12));
assertEquals(12, test.solution(5, 4));
assertEquals(40, test.solution(10, 5));
assertEquals(352, test.solution(17, 23));
assertEquals(30467460, test.solution(3, 15233730));

참고 사이트

본문 참조

댓글 공유

https://programmers.co.kr/learn/courses/30/lessons/42587


코드

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
public int solution(int[] priorities, int location) {
int answer = 0;

LinkedList<Integer> queue = new LinkedList<Integer>();
for (int i = 0; i < priorities.length; i++) {
queue.offer(priorities[i]);
}

while (queue.size() != 0) {
for (int i = 1; i < queue.size(); i++) {
if (queue.peek() < queue.get(i)) {
queue.offer(queue.poll());

if (location == 0) {
location = queue.size() - 1;
} else {
--location;
}

i = 0;
}
}

answer++;
if (location == 0) {
break;
}

--location;
queue.poll();
}

return answer;
}

흐름

  1. 우선순위가 담긴 배열(priorities)을 Queue에 저장

  2. 큐에 첫 번째 값을 꺼내서 그 뒤 값들과 비교

  3. 첫 번째 값이 뒤에 있는 값들 보다 작다면 큐에 맨 뒤로 보냄

  4. 출력해야 되는 프린터의 index를 조정

    • index가 0 이라면 맨 뒤로 이동했을테니 큐 크기 - 1;
    • 아니라면 앞으로 한 칸 이동 했을테니 -1
  5. 맨 뒤로 이동 시켰으면 맨 앞에 값이 변경 됐으므로 처음부터 다시 검사

  6. 큐의 0 번째 인덱스가 가장 큰 값이라면 count를 증가시키고 출력해야 하는 프린터의 위치를 확인

  7. 출력해야하는 프린터가 맨 앞이라면 break로 반복문을 빠져나와 프로그램 종료

  8. 아니라면 맨 앞에 값을 삭제하고 index를 -1 해서 한 칸 앞으로 당김

  9. 끝날 때 까지 반복


결과

번호 속도
테스트 1 통과 (3.17ms, 52.8MB)
테스트 2 통과 (3.41ms, 52.6MB)
테스트 3 통과 (1.33ms, 52.6MB)
테스트 4 통과 (1.15ms, 50.8MB)
테스트 5 통과 (0.85ms, 52.2MB)
테스트 6 통과 (1.44ms, 52.7MB)
테스트 7 통과 (1.58ms, 50.4MB)
테스트 8 통과 (3.89ms, 52.9MB)
테스트 9 통과 (1.04ms, 52.8MB)
테스트 10 통과 (1.57ms, 52.3MB)
테스트 11 통과 (3.90ms, 52.7MB)
테스트 12 통과 (1.07ms, 52.3MB)
테스트 13 통과 (4.36ms, 52.1MB)
테스트 14 통과 (0.83ms, 52MB)
테스트 15 통과 (0.95ms, 50.4MB)
테스트 16 통과 (1.21ms, 50.3MB)
테스트 17 통과 (2.79ms, 50.7MB)
테스트 18 통과 (0.94ms, 50.7MB)
테스트 19 통과 (2.61ms, 53.1MB)
테스트 20 통과 (1.51ms, 52.3MB)

테스트 케이스

1
2
3
4
assertEquals(1, test.solution(new int[] {2, 1, 3, 2}, 2));
assertEquals(5, test.solution(new int[] {1, 1, 9, 1, 1, 1}, 0));
assertEquals(6, test.solution(new int[] {2, 2, 2, 1, 3, 4}, 3));
assertEquals(2, test.solution(new int[] {1, 2, 3, 2}, 3));

댓글 공유

팩토리

  • 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 오브젝트
  • 디자인 패턴의 추상 팩토리 패턴이나 팩토리 메서드 패턴과는 다름
  • 오브젝트를 생성하는 쪽과 생성된 오브젝트를 사용하는 쪽의 역할과 책임을 분리하려는 목적으로 사용하는 것
  • 애플리케이션의 컴포넌트 역할을 하는 오브젝트와 애플리케이션의 구조를 결정하는 오브젝트를 분리한다는 데 의미가 있음

제어관계 역전

  • 일반적인 프로그램의 흐름은

    1. 프로그램의 시작지점(main method 등)에서 다음에 사용할 오브젝트 결정
    2. 결정한 오브젝트 생성
    3. 만들어진 오브젝트에 있는 메서드를 호출
    4. 메서드 안에서 다음에 사용할 것을 결정하고 호출
  • 이는 모든 오브젝트가 능동적으로 자신이 사용할 클래스를 결정

  • 언제 어떻게 그 오브젝트를 만들지를 스스로 관장

  • 모든 종류의 작업을 사용하는 쪽에서 제어하는 구조

  • 제어의 역전(Ioc)이란 이런 제어 흐름의 개념을 거꾸로 뒤집는 것

스프링 Ioc ApplicationContext 왜 사용?

  • 범용적이고 유연한 방법으로 Ioc 기능을 확장하기 위해
  1. 애플리케이션이 고도화되면 Ioc를 적용한 오브젝트도 계속 추가 될텐데 매번 어떤 팩토리 클래스를 사용해야 하는 지 알아야하고 필요할 때마다 팩토리 오브젝트를 생성해야하는 번거로움이 있음
    applicationContext는 알거나 직접 사용할 필요가 없고 일관된 방식으로 원하는 오브젝트를 가져올 수 있음

  2. 생성, 관계설정만 하는 것이 아닌 만들어지는 방식, 시점, 전략, 부가적으로 자동생성, 오브젝트에 대한 후 처리 등 효과적으로 활용할 수 있는 다양한 기능을 제공

  3. bean을 검색하는 다양한 방법을 제공


스프링 Ioc 용어 정리

빈(bean)

  • 스프링이 Ioc 방식으로 직접 그 생성과 제어를 담당하는 오브젝트

빈 팩토리(bean factory)

  • 스프링의 Ioc를 담당하는 핵심 컨테이너
  • 빈을 등록, 생성, 조회하고 돌려주고 부가적인 빈을 관리하는 기능을 담당
  • 보통 applicationContext를 사용

애플리케이션 컨텍스트(application context)

  • 빈 팩토리를 확장한 Ioc 컨테이너
  • 빈 팩토리에 스프링이 제공하는 각종 부가 서비스를 추가로 제공한 것

스프링이 싱글톤으로 빈을 생성하는 이유

  • 매번 클라이언트에서 요청이 올 때 마다 각 로직을 담당하는 오브젝트를 새로 만들어서 사용한다면 서버가 부하를 감당하기 어려움
  • 그래서 제한된 수(보통 한 개)의 오브젝트만 만들어서 사용하도록 싱클톤을 사용

Java 싱글톤 패턴의 한계

  1. private 생성자를 갖고 있기 때문에 상속 할 수 없음
  2. 초기화 과정에서 생성자 등을 통해 사용할 오브젝트를 다이나믹하게 주입하기도 힘들기 때문에 필요한 오브젝트는 직접 오브젝트를 만들어서 사용 할 수 밖에 없어 테스트가 힘듬
  3. 서버에서 클래스 로더를 어떻게 구성하고 있느냐에 따라 싱글톤 클래스여도 하나 이상의 오브젝트가 만들어질 수 있음
  4. 싱글톤의 static 메서드를 이용해 전역 상태로 사용되기 쉬우므로, 아무 객체나 자유롭게 접근하고 수정하고 공유할 수 잇는 전역 상태를 갖는 것은 객체지향 프로그래밍에서 권장되지 않음

싱글톤 레지스트리

  • 스프링에서 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능

싱글톤 레지스트리의 장점

  • 오브젝트 생성에 대한 모든 권한은 Application Context에 있기 때문에 static 메서드와 private 생성자를 사용하는 것이 아닌 평범한 자바 클래스를 싱글톤으로 활용하게 해줌
  • 스프링이 빈을 싱글토으로 만드는 것은 결국 오브젝트의 생성 방법을 제어하는 Ioc 컨테이너로서의 역할

오브젝트의 상태

  • 멀티스레드 환경이라면 여러 스레드가 동시에 싱글톤에 접근해서 사용할 수 있으므로 상태 관리에 주의해야 함
  • 기본적으로 상태정보를 내부에 갖고 있지 않는 무상태 방식으로 생성되어야 함
  • 서로 값을 덮어쓰고자신이 저장하지 않은 값을 읽어올 수 있기 때문에 기본적으로 인스턴스 필드의 값을변경하고 유지하는 상태유지 방식으로 만들지 않음
  • 상태가 없는 방식으로 만드는 경우 생성한 정보는 파라미터와 지역변수, 리턴 값으로 이용함
  • 파라미터나 지역변수는 매번 새로운 값을 저장할 독립적인 공간이 만들어지기 때문에 싱글톤이라고 해도 여러 스레드가 변수의 값을 덮어 쓸일이 없음
  • 읽기전용 정보인 자신이 사용하는 다른 싱글톤 빈을 저장하려는 용도라면 인스턴스 변수를 사용해도 무방함

의존관계

  • A -> B 이면 B가 변하면 그것이 A에도 영향을 미친다는 것
  • 어린 자식은 부모에 의존적이므로 부모가 변하면 자식도 변하게 됨
  • 의존관계에는 방향성이 있음 (자식 -> 부모)
  • 자바에서는 Interface를 통해 의존관계를 줄일 수 있음, 이것이 낮은 결합도
  • 모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존관계말고 런타임 시에 오브젝트 사이에서 만들어지는 의존관계도 있음
  • 런타임 의존관계 혹은 오브젝트 의존관계라고 함

의존 오브젝트

  • 프로그램이 시작되고 오브젝트가 만들어지고 런타임 시에 의존관계를 맺는 대상, 즉 실제 사용대상인 오브젝트

의존관계 주입

  • 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업
    1. 클래스 모델이나 코드에는 런타임 시점에 의존관계가 드러나지 않으므로 인터페이스에만 의존하고 있어야 함
    2. 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정
    3. 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어짐
  • 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제 3의 존재가 있다는 것
    • application context
    • 빈 팩토리
    • Ioc 컨테이너 등

의존관계 검색

  • 외부로부터의 주입이 아니라, 스스로 검색
  • 런타임 시 의존관계를 맺을 오브젝트를 검색하는 것과 오브젝트의 생성 작업은 외부 컨테이너에게 Ioc로 맡기지만, 이를 가져올 떄는 메서드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용
  • 미리 준비된 메서드를 호출하면 되니 단순히 요청으로 보이겠지만, 이런 작업을 일반화한 스프링의 application context라면 미리 정해놓은 일므을 전달해서 그 이림에 해당하는 오브젝트를 찾게 됨.
    • getBean()
  • 의존관계 검색 방식에서는 검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없음
  • 반면 의존관계를 주입하기 위해선 생성과 초기화 권한을 갖고 있어야하고, 그러려면 Ioc 방식으로 컨테이너에서 생성되는 오브젝트, 즉 빈이어야 하기 때문

DI를 원하는 오브젝트는 먼저 자기 자신이 컨테이너가 관리하는 빈이 돼야 한다는 사실을 잊지 말자.

의존관계 주입의 장점

  1. 코드에는 런타임 클래스에 대한 의존관계가 나타나지 않음
  2. 인터페이스를 통해 결합도가 낮은 코드가 되므로 의존관계에 있는 대상이 바뀌거나 변경되더라도 자신이 영향 받지 않음
  3. 변경을 통한 다양한 확장에는 자유로움

1장 마무리

스프링이란, 어떻게 오브젝트가 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는 지에 관심을 갖는 프레임워크

댓글 공유

https://programmers.co.kr/learn/courses/30/lessons/42586


소스

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
public int[] solution(int[] progresses, int[] speeds) {
int start = 0;
List<Integer> list = new ArrayList<Integer>();
while (start != progresses.length) {
int deploy = 0;
for (int i = start; i < progresses.length; i++) {
progresses[i] += speeds[i];
}

for (int i = start; i < progresses.length; i++) {
if (progresses[start] < 100) {
break;
}

if (progresses[i] >= 100) {
++start;
++deploy;
}
}

if (deploy != 0) {
list.add(deploy);
}
}

int[] answer = list.stream().mapToInt(i -> i).toArray();
return answer;
}

흐름

  1. 기능개발이 완료된 인덱스부터 반복하기 위해 start 변수 생성

  2. 기능개발이 끝날 때 까지 반복

  3. 개발이 안 끝난 기능 index 부터 돌면서 작업 속도를 더 함

  4. 개발이 안 끝난 기능 index 부터 돌면서 첫 번째 기능이 개발 됐는지 확인하고 안 끝났으면 3 번으로 돌아감

  5. 첫 번째 기능이 완료 되었으면 그 다음 기능개발을 위해 ++start 하고, deploy된 기능을 카운팅하는 deploy 변수를 증가

  6. deploy한 기능이 있는 경우에만 list에 저장

  7. ArrayList를 int[]로 변경


다른 분의 소스

1
2
3
4
5
6
7
8
9
10
11
public int[] solution(int[] progresses, int[] speeds) {
int[] dayOfend = new int[100];
int day = -1;
for(int i=0; i<progresses.length; i++) {
while(progresses[i] + (day*speeds[i]) < 100) {
day++;
}
dayOfend[day]++;
}
return Arrays.stream(dayOfend).filter(i -> i!=0).toArray();
}

로직

  • 기능 갯수 만큼 돌면서 진도율과 날짜 * 개발 속도를 비교해서 100 보다 작으면 다음날로 증가시키고 크면 그 날짜에 저장되는 갯수를 증가시킴

  • 대박


결과

1 번

번호 속도
테스트 1 통과 (4.53ms, 50.8MB)
테스트 2 통과 (5.07ms, 50.6MB)
테스트 3 통과 (5.09ms, 52.8MB)
테스트 4 통과 (5.29ms, 52.4MB)
테스트 5 통과 (8.36ms, 55MB)
테스트 6 통과 (4.95ms, 51.2MB)
테스트 7 통과 (5.58ms, 52.6MB)
테스트 8 통과 (5.18ms, 50.7MB)
테스트 9 통과 (5.44ms, 52.5MB)
테스트 10 통과 (5.22ms, 50.5MB)

2 번

번호 속도
테스트 1 통과 (6.60ms, 51.4MB)
테스트 2 통과 (6.83ms, 53.1MB)
테스트 3 통과 (6.92ms, 52.5MB)
테스트 4 통과 (6.70ms, 52.8MB)
테스트 5 통과 (19.88ms, 52.4MB)
테스트 6 통과 (5.05ms, 50.7MB)
테스트 7 통과 (7.01ms, 52.5MB)
테스트 8 통과 (4.68ms, 50.5MB)
테스트 9 통과 (6.95ms, 52.2MB)
테스트 10 통과 (5.31ms, 50.8MB)
  • 두 소스 모두 Arrays.stream을 사용하지 않고 반복해서 저장하면 1초 대로 줄어듬.
  • 확실히 스트림이 느린 듯함.

테스트 케이스

1
2
3
assertArrayEquals(new int[] {2,1}, test.solution(new int[] {93, 30, 55}, new int[] {1, 30, 5}));
assertArrayEquals(new int[] {3}, test.solution(new int[] {0, 30, 55}, new int[] {1, 99, 99}));
assertArrayEquals(new int[] {1,1,1}, test.solution(new int[] {3, 2, 1}, new int[] {1, 1, 1}));

전체 소스

https://github.com/jungguji/algorithm_training/blob/master/src/main/java/algorithm/programmers/level2/stackqueue/%EB%8B%A4%EB%A6%AC%EB%A5%BC_%EC%A7%80%EB%82%98%EB%8A%94_%ED%8A%B8%EB%9F%AD.java

댓글 공유

https://programmers.co.kr/learn/courses/30/lessons/49993?language=java


소스

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
57
58
59
public int solution(String skill, String[] skill_trees) {
char[] ch = skill.toCharArray();

List<ArrayList<Skill>> l = new ArrayList<ArrayList<Skill>>();
for (String element : skill_trees) {
ArrayList<Skill> list = new ArrayList<Skill>();

for (int i = 0; i < ch.length; i++) {
Skill s = new Skill();
s.skill = ch[i];
s.index = element.indexOf(ch[i]);

if (s.index != -1) {
list.add(s);
}
}

l.add(list);
}

int answer = 0;
for (ArrayList<Skill> list : l) {
Collections.sort(list, new Comparator<Skill>() {
@Override
public int compare(Skill o1, Skill o2) {
return o1.getIndex().compareTo(o2.getIndex());
}
});

StringBuilder sb = new StringBuilder();
for (Skill s : list) {
sb.append(s.getSkill());
}

if (skill.startsWith(sb.toString())) {
answer++;
}
}

return answer;
}

public class Skill {
private int index;
private char skill;

public Integer getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public char getSkill() {
return skill;
}
public void setSkill(char skill) {
this.skill = skill;
}
}

흐름

  1. 스킬트리에 있는 스킬들을 선행스킬 만큼 반복한다.

  2. 돌면서 스킬들에서 선행스킬의 index와 선행스킬명을 저장하는 클래스(Skill)에 저장하고 그 클래스를 List에 저장한다.

    • ex) List = [{index = 1, skill = A}, {index = 7, skill = Q}, …]
  3. 선행스킬명을 저장한 List를 다른 List(AllSkillList)에 저장한다.

    • 전체 스킬을 저장해야 하므로
    • ex) AllSkillList = [List1, List2, List3, …]
  4. 전체 스킬이 저장된 List(AllSkillList)를 돌면서 저장된 List의 Skill의 Index 순으로 정렬 시킨다.

  5. 정렬된 List를 돌면서 StringBuilder에 스킬명을 담는다.

  6. 입력받은 선행스킬과 비교하여 일치하면 answer을 증가 시킨다.


다른 분의 소스

1
2
3
4
5
6
7
8
9
10
11
12
13
public int solution(String skill, String[] skill_trees) {
int answer = 0;
ArrayList<String> skillTrees = new ArrayList<String>(Arrays.asList(skill_trees));
Iterator<String> it = skillTrees.iterator();

while (it.hasNext()) {
if (skill.indexOf(it.next().replaceAll("[^" + skill + "]", "")) != 0) {
it.remove();
}
}
answer = skillTrees.size();
return answer;
}

로직

  • 스킬트리에 있는 스킬들 중 선행스킬이 아닌 녀석들을 없애버리고 선행스킬과 일치하는 지 확인해서 일치하지 않으면 List에서 삭제하고 List의 갯수를 return 하는 것으로 끝을 낸다.

  • ex) “CBD” 인 경우 “BACDE”에서 “C”, “B”, “D”가 아닌 녀석들은 삭제되서 “BCD”가 남지만 “CBD”.indexOf(“BCD”) 할 경우 “CBD”에 “BCD”가 존재 하지 않으므로 1을 return 하고 -1은 0이 아니므로 List에서 삭제된다.

  • “BDA” 인 경우엔 “CBD”.indexOf(“BD”) 할 경우 “CBD”에 존재하지만 1 번째 index에 존재하므로 1을 return 하고 List에서 삭제된다.

  • 직관적이고 가독성이 좋다.

  • 정규표현식 활용도 굿


결과

1 번

번호 속도
테스트 1 통과 (1.40ms, 52.1MB)
테스트 2 통과 (1.37ms, 50.1MB)
테스트 3 통과 (1.29ms, 52.7MB)
테스트 4 통과 (1.11ms, 52.3MB)
테스트 5 통과 (1.42ms, 50.6MB)
테스트 6 통과 (1.33ms, 52.4MB)
테스트 7 통과 (1.38ms, 50.5MB)
테스트 8 통과 (1.38ms, 50.8MB)
테스트 9 통과 (1.46ms, 52MB)
테스트 10 통과 (1.26ms, 52.4MB)
테스트 11 통과 (1.36ms, 54.3MB)
테스트 12 통과 (1.72ms, 52.5MB)
테스트 13 통과 (1.27ms, 52.3MB)
테스트 14 통과 (1.31ms, 52.2MB)

2 번

번호 속도
테스트 1 통과 (17.73ms, 54.5MB)
테스트 2 통과 (18.75ms, 52.2MB)
테스트 3 통과 (17.83ms, 52.4MB)
테스트 4 통과 (19.98ms, 54.3MB)
테스트 5 통과 (20.10ms, 52.3MB)
테스트 6 통과 (20.89ms, 52.5MB)
테스트 7 통과 (20.94ms, 54.3MB)
테스트 8 통과 (18.68ms, 52.2MB)
테스트 9 통과 (19.60ms, 52.8MB)
테스트 10 통과 (20.75ms, 54.4MB)
테스트 11 통과 (18.02ms, 52.3MB)
테스트 12 통과 (18.70ms, 54.3MB)
테스트 13 통과 (18.79ms, 52.5MB)
테스트 14 통과 (19.85ms, 54.8MB)
  • 2 번의 경우 Arrays.asList(skill_trees) 와 replace 때문에 속도가 살짝 느려진 것으로 판단된다.

테스트 케이스

1
2
3
4
5
6
assertEquals(3, test.solution("CBD", new String[] {"BACDE", "CBADF", "AECB", "BDA", "ASF", "BDF","CEFD"}));
assertEquals(4, test.solution("C", new String[] {"BACDE", "CBADF", "AECB", "BDA"}));
assertEquals(2, test.solution("CBD", new String[] {"C", "D", "CB", "BDA"}));
assertEquals(2, test.solution("AC", new String[] {"ABC", "CA", "ATEW", "SFCQTA"}));
assertEquals(2, test.solution("ACHQ", new String[] {"TWER", "FGCHQEA", "ATEW", "SFCQTA"}));
assertEquals(4, test.solution("CBDK", new String[] {"CB", "CXYB", "BD", "AECD", "ABC", "AEX", "CDB", "CBKD", "IJCB", "LMDK"}));
  • 임의로 만든 테스트 케이스 이므로 위 테스트 케이스를 통과해도 시험에서 통과하지 못할 가능성이 있다.

댓글 공유

관심사 분리

  • 관심이 같은 것 끼리는 하나의 객체 안으로 또는 친한 객체로 모이게하고,
    관심이 다른 것은 가능한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것

템플릿 메서드 패턴

  • 슈퍼클래스에 기본적인 로직의 흐름을 만들고,
    그 기능의 일부를 추상 메서드나 오버라이딩 가능한 protected 메서드 등으로 만든 뒤 서브클래스에서 이런 메서드를 필요에 맞게 구현해서 사용하도록 하는 방법

팩토리 메서드 패턴

  • 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것

중요한 건 디자인 패턴이 아닌 상속구조를 통해 성격이 다른 관심사항을
분리한 코드를 만들어내고, 서로 영향을 덜 주도록 했는지를 이해하는 것


상속을 통한 확장의 문제점

  1. 만약 다른 목적을 위해 이미 상속을 사용하고 있다면?

  2. 상속을 통한 상하위 클래스의 관계는 생각보다 밀접

  3. 서브클래스는 슈퍼클래스의 기능을 직접 사용할 수 있음
    그래서 슈퍼클래스 내부의 변경이 있을 때 모든 서브클래스를 함께 수정하거나 다시 개발해야 할 수도 있음
    반대로 그런 변화에 따른 불편을 주지 않기 위해 슈퍼클래스가 더 이상 변화하지 않도록 제약을 가해야 할지도 모름

추상 클래스를 만들고 이를 상속한 서브클래스에서
변화가 필요한 부분을 바꿔서 쓸수 있게 만든 이유는
변화의 성격이 다른 것을 분리해서 서로 영향을 주지 않은 채로
각각 필요한 시점에 독립적으로 변경할 수 있게 하기 위해서

그렇다면 아예 클래스로 따로 때 버린다면?

  • 코드로 새로 만든 클래스에 종속되어 버림
  • 상속 했을 때 처럼 코드 수정없이 기능을 변경할 방법이 없음

클래스를 통한 확장의 문제점

  1. 현재 a 메서드를 사용해 기능을 제공하고 있는데
    만약 a 메서드 대신 b 메서드를 사용하도록 수정 된다면
    일일이 a 메서드를 사용한 수십 수백개의 메서드를 수정해야 함

  2. 기능을 제공하는 클래스가 어떤 것인지 클라이언트가 구체적으로 알고 있어야 함
    만약 다른 클래스를 구현하면 또 클라이언트 자체를 수정해야 함

  3. 근본적인 원인은 클라이언트가 바뀔 수 있는 정보(db 커넥션 등)을 가져오는 클래스에 대해 너무 많이 알고 있기 때문

해결

  • 두 클래스가 서로 긴밀하게 연결되지 않도록 중간에 추상적인 연결고리를 만드는 것(java의 interface)

  • 클라이언트 입장에서는 인터페이스를 상속 받는 클래스의 오브젝트라면 어떤 클래스로 만들었건
    메서드를 호출하기만 하면 약속된 오브젝트를 만들어서 돌려줄 것으로 기대함


Interface를 통한 확장의 문제점

  1. 아직도 클래스의 생성자를 호출해서 오브젝트를 생성하는 코드가 남아있음

    • 초기에 한번 어떤 클래스의 오브젝트를 사용할지를 결정하는 생성자의 코드는 남아 있음
    • ex) conntion = new DConnectionMaker();
  2. 결국 클라이언트가 어떤 구현 클래스의 오브젝트를 이용하게 할지 결정하는 관심사가 남아 있음

해결

  • 오브젝트와 오브젝트 사이에 관계를 설정해야함

  • 오브젝트 사이의 관계는 런타임 시에 한쪽이 다른 오브젝트의 레퍼런스를 갖고 잇는 방식으로 만들어짐(보통 알고 있는 인스턴스 생성 방식)

    • ex)conntion = new DConnectionMaker();
    • DConnectionMaker의 오브젝트의 레퍼런스를 클라이언트의 connection 변수에 넣어서 사용하게 함

클래스 사이의 관계는 코드에 다른 클래스 이름이 나타나기 때문에 만들어지는 것

오브젝트 사이의 관계는 코드에서는 특정한 클래스를 전혀 알지 못하더라도 해당 클래스가 구현한 인터페이스를 사용했다면, 그 클래스의 오브젝트를 인터페이스 타입을 받아서 사용 할 수 있음

이것이 객체지향의 다형성


개방 폐쇄 원칙

  • 클래스나 모듈은 확장에는 열려 있어야하고 변경에는 닫혀 있어야 함

높은 응집도와 낮은 결합도

높은 응집도

  • 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있음
  • 작업은 항상 전체적으로 일어나고 무엇을 변경할지 명확하며, 그것이 클라이언트의 다른 로직에 수정을 요구하지 않을 뿐더라
    기능에 영향을 주지 않음
  • 변경이 일어난 경우에 이를 검증하려고 하면 변경한 구현 클래스만 직접 테스트 해보는 것으로 충분하기 때문
  • 응집도를 높인 덕분

낮은 결합도

  • 하나의 오브젝트가 변경이 일어날 떄에 관계를 맺고 잇는 다른 오브젝트에게 변화를 요구하는 정도
  • 하나의 변경이 발생할 때 여타 모듈과 객체로 변경에 대한 요구가 전파되지 않는 상태
  • 책임과 관심사가 다른 오브젝트 또는 모듈과는 낮은 결합도를 유지해야 함
  • 느슨하게 연결된 형태를 유지하는 것이 바람직
  • 꼭 필요한 최소한의 방법만 제공
  • 결합도가 낮아지면 대응하는 속도가 높아지고 구성이 깔금해짐

전략패턴

  • 자신의 기능 맥락(context)에서 필요에 따라 변경이 필요한 알고리즘을 인터페스를 통해 통째로 외부로 분리시키고 이률 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔 사용할 수 있게 하는 디자인 패턴

댓글 공유

문제

토이 프로젝트 진행 중 문자열 하나만 서버로 넘겨 받아 처리하면 되는 상황이여서 아래 코드 처럼 dataType을 ‘text’로 설정했다.

1
2
3
4
5
6
7
8
9
10
$.ajax({
type:"GET"
, url: "/test"
, data: data
, dataType : 'text'
, cache: false
, success: function(data) {
drawGrid(data);
}
});

허나 위 처럼 하니, success에서 받는 data가 json object가 아닌 String type으로 받아지는 문제가 발생했다.


해결

1
2
3
4
5
6
7
8
9
10
11
$.ajax({
type:"GET"
, url: "/test"
, data: data
, contentType: "application/json"
, dataType : 'JSON'
, cache: false
, success: function(data) {
drawGrid(data);
}
});

결국 위 처럼 dataType을 ‘JSON’으로,
contentType도 ‘JSON’으로 변경하고
Controller도 json으로 변경하니

success에서 받는 dat도 정상적으로 JSON Object로 넘어왔다.


결론

받는 데이터가 json이라면 전송 때 json일 필요가 없어도 json으로 맞춰주자.

댓글 공유

Junggu Ji

author.bio


author.job