스프링의 정의

자바 엔터프라이즈 개발을 편하게 해주는 오플ㄴ소스 경량급 애플리케이션 프레임워크

이일민토비의 스프링

애플리케이션 프레임워크

  • 특정 계층이나, 기술, 업무 분야에 국한되지 않고 애플케이션의 전 영역을 포괄하는 범용적인 프레임워크
  • 애플리케이션 개발의 전 과정을 빠르고 편리하며 효율적으로 진행하는데 목표를 두는 프레임워크
  • 단지 여러 계층의 다향한 기술을 한데 모아뒀기 때문에 그렇게 불리는 게 아님

스프링의 일차적인 존재 목적은 핵심 기술에 담긴 프로그래밍 모델을 일관되게 적용해서 엔터프라이즈 애플리케이션 전 계층과 전 여영ㄱ에 전략과 기능을 제공해줌으로써 애플리케이션을 편리하게 개발하게 해주는 애플리케이션 프레임워크로 사용되는 것

경량급

  • 자체가 가렵다거나 작은 규모의 코드로 이뤄졌다는 뜻이 아님
  • 불필요하게 무겁지 않다는 의미
  • EJB에 비해 단순한 개발툴과 기본적인 개발환경으로도 엔터프라이즈 개발에서 필요로 하는 주요한 기능을 갖춘 애프리케이션을 개발하기 충분

자바 엔터프라이즈 개발을 편하게

  • 개발자가 복잡하고 실수하기 쉬운 로우레벨 기술에 많은 신경을 쓰지 않게함
  • 애플리케이션의 핵심인 사용자의 요구사항, 즉 비즈니스 로직을 빠르고 효과적으로 구현하는 것
  • 스프링이 제공하는 기술이 아니라 자신이 작성하는 애플리케이션의 로직에 더 많은 관심과 시간을 투자하게 함
  • 초기에 기본 설정과 적용 기술만 선택하고 준비해두면 이후에 개발자가 신경 쓸 일이 없음

결국 스프링의 목적은 스프링을 할용해서 엔터프라이즈 애플리케이션 개발을 편하게 하는 것
그렇다면 스프링은 어떻게 복잡한 엔터프라이즈 개발을 편하게 해주는가?


복잡함의 근본적인 이유

  1. 기술적인 제약조건과 요구사항이 늘어남
  2. 애플리케이션이 구현해야 할 핵심기능인 비즈니스 로직의 복잡함이 증가함
  • 자바 엔터프라이즈 시스템 개발이 어려운 가증 큰 이유는 근본적인 비즈니스 로직과 엔터프라이즈 기술이라는 두 가지 복잡함이 한데 얽혀 있기 때문

해결책

  • 기술의 적용 사실이 코드에 직접 반영되지 않는 비침투적 기술 사용

  • 어딘가에는 기술의 적요에 따라 필요한 작업을 해줘야하겠지만, 애플리케이션 코드 여기저기에 불쑥 등장하거나, 코드의 설꼐와 구현 방식을 제한하지 않는다는 게 비침투적인 기술의 특징

  • 반대로 침투적인 기술은 어떤 기술을 적용했을 떄 그 기술과 관련된 코드나 규약 등이 코드에 등장하는 경우

  • 즉 스프링의 기본적인 전량근 비즈니스 로직을 담은 애플리케이션 코드와 엔터프라이즈 기술을 처리하는 코드를 분리시키는 것

기술적 복잡함을 상대하는 전략

서비스 추상화

  • 일관성 없는 기술과 서버환경의 변화에 대한 스프링의 공략 방법은 서비스 추상화
  • 추상화를 통해 로우레벨의 기술 구현 부분과 기술을 사용하는 인터페이스를 분리하고, 환경과 세부기술에 독립적인 접근 인터페이스를 제공하는 것이 가장 좋은 해결책

AOP

  • 비즈니스 로직 전후로 경계가 설정돼야하는 트랜잭션
  • 비즈니스 로직에 대한 보안 적용
  • 계층 사이에 주고받는 데이터와 예외의 일괄 변환이나 로징이나 감사 기능 등
  • 이런 기술과 비즈니스 로직의 혼재로 발생하는 복잡함을 해결하기 위한 스프링의 접근 방법이 AOP

비즈니스와 애플리케이션 로직의 복잡함을 상대하는 전략

객체지향과 DI

  • 기술적인 복잡함을 효과적으로 다루게 해주는 기법은 모두 DI를 바탕으로 함
  • 서비스 추상화, 템플릿/콜백, AOP 등
  • 그리고 DI는 객체지향 프로그래밍 기법일 뿐
  • DI를 적용해서 오브젝트를 분리하고, 인터페이스를 동비하고, DI로 관계를 연결
  • 결국 DI는 좋은 오브젝트 설계의 결과물이기도 하지만, 반대로 DI를 적용하다보면 객체지향 설계의 원칙을 잘 따르고 그 장점을 살린 설계가 나올 수 있음

결국 스프링의 기술과 전략은 객체지향이라는 자바 언어가 가진 강력한 도구를 극대화해서 사용할 수 있도록 돕는 것 이고 스프링은 단지 거들 뿐이다.

댓글 공유

문제

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


코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int solution(int[] people, int limit) {
Arrays.sort(people);

int left = 0;
int right = people.length - 1;

int answer = 0;
while (left <= right) {
int sum = people[left] + people[right];

if (sum <= limit) {
++left;
}

++answer;
--right;
}

return answer;
}

흐름

  1. 배열을 정렬

  2. 배열 내에서 제일 작은 값이랑 제일 큰 값을 더해서 limit이 넘어가는 지 확인

  3. 넘어가면 제일 작은 값을 증가

  4. 안 넘어가면 제일 큰 값 혼자 빠져야하므로 answer을 증가시키고 right를 감소 시켜서 인덱스를 한칸 땡김

  5. right가 left 보다 작아질 때 까지 반복
    ex) [50,50,70,80] 인 경우 left = 0, right = 4 인데 위 코드르 반복하면 right가 점점 줄어들어 left와 만날 때 끝


결과

정확성 테스트

번호 속도
테스트 1 통과 (1.92ms, 52.7MB)
테스트 2 통과 (1.56ms, 51.4MB)
테스트 3 통과 (1.66ms, 51MB)
테스트 4 통과 (2.02ms, 50.7MB)
테스트 5 통과 (1.42ms, 53MB)
테스트 6 통과 (1.37ms, 50.9MB)
테스트 7 통과 (1.46ms, 53.1MB)
테스트 8 통과 (0.95ms, 52.6MB)
테스트 9 통과 (1.15ms, 52.5MB)
테스트 10 통과 (2.01ms, 52.4MB)
테스트 11 통과 (1.68ms, 52.7MB)
테스트 12 통과 (1.78ms, 52.7MB)
테스트 13 통과 (2.07ms, 52.5MB)
테스트 14 통과 (1.46ms, 50.5MB)
테스트 15 통과 (1.22ms, 50.1MB)

효율성 테스트

번호 속도
테스트 1 통과 (11.43ms, 56.1MB)
테스트 2 통과 (11.63ms, 55.7MB)
테스트 3 통과 (11.88ms, 53.7MB)
테스트 4 통과 (8.84ms, 56.3MB)
테스트 5 통과 (11.08ms, 55.7MB)

테스트 케이스

1
2
3
4
5
6
assertEquals(3, test.solution(new int[] {70, 50, 80, 50}, 100));
assertEquals(3, test.solution(new int[] {70, 80, 50}, 100));
assertEquals(2, test.solution(new int[] {40, 40, 80}, 160));
assertEquals(2, test.solution(new int[] {20, 50, 50, 80}, 100));
assertEquals(5, test.solution(new int[] {40,50,60,70,80,90}, 100));
assertEquals(2, test.solution(new int[] {40,40,40}, 100));

댓글 공유

문제

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


코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public String solution(String number, int k) {
char[] toCharArray = number.toCharArray();

StringBuilder answer = new StringBuilder();

int idx = 0;
for(int i = 0; i < number.length() - k; i++) {
int max = 0;

for(int j = idx; j <= k + i; j++) {
int ch = toCharArray[j] - '0';
if(max < ch) {
max = ch;
idx = j + 1;
}
}

answer.append(max);
}

return answer.toString();
}

흐름

  1. 문자열에서 k 만큼 빼야하니 당연히 문자열 length - k 만큼 반복
  2. 가장 큰 수의 인덱스를 구해서 그 인덱스부터 한 칸씩 밀려야 하니 k + i 한 값 까지 반복
  3. 가장 큰 수의 인덱스부터 반복해야하니 범위 내에서 가장 큰 수를 구해서 그 수의 인덱스를 저장하고 여기서 구한 인덱스를 2번에서 사용
  4. 가장 큰 수를 저장하고 반복이 끝나면 리턴

테스트 케이스

1
2
3
4
5
6
7
assertEquals("23", test.solution("123", 2));
assertEquals("34", test.solution("1234", 2));
assertEquals("94", test.solution("1924", 2));
assertEquals("3234", test.solution("1231234", 3));
assertEquals("775841", test.solution("4177252841", 4));
assertEquals("9", test.solution("9999999999", 9));
assertEquals("93231357719", test.solution("9312131357719", 5));

참고 사이트

댓글 공유

문제

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


코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int solution(String[][] clothes) {
Map<String, Integer> map = new HashMap<String, Integer>();

for (String[] s : clothes) {
int value = 1;
String key = s[1];
if (map.containsKey(key)) {
value = map.get(key);
}

map.put(key, ++value);
}

int answer = 1;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
answer *= entry.getValue();
}

return answer - 1;
}

흐름

  1. [[“yellow_hat”, “headgear”], [“blue_sunglasses”, “eyewear”], [“green_turban”, “headgear”]], clothes안에 이런 식으로 옷이름, 종류 로 저장되어 있으므로 chothes 크기 만큼 반복하면서 옷 종류를 꺼냄

  2. 옷 종류를 key로 하는 Map이 있는 지 확인

  3. 있으면 값을 증겨 시켜야 하므로 value를 꺼내옴

  4. ++해서 값을 증가 시켜서 저장하고, 없으면 해당 key로 새로 저장

  5. 순열과 조합 공식을 바탕으로 value를 모두 곱함

  6. 문제에서 최소 1 개의 옷은 입는다고 하였으므로, 옷을 모두 안입는 경우를 제외하기 위해 -1을 함

조합 공식

Every interaction is both precious and an opportunity to delight.

서로 다른 n개의 원소에서 순서에 상관없이 r개를 뽑을 때, 이를 n개에서 r개를 택하는 조합이라고 한다. 이 조합은 순열과 다른 개념으로 순서 차이가 중요하다.

$$ nCr = nPr / r! = n! / (n-r)!r! $$

  • 이 공식으로 위 문제를 풀어보면 헤어 2개, 안경 1개 이므로
  • 헤어 2개에서는 입을경우, 안 입을 경우 2 가지이고 이를 수식으로 표현하면
  • 0개를 뽑는 경우 = $$ 2C0 = 2P0 / 0! = 2! / (2-0)!0! = 2 / 2 = 1 $$
  • 이 되고, 나머지의 경우도 계산하면 $$ 1 + 2 = 3 $$ 이 된다.
  • 안경도 마찬가지로 입을 경우, 안 입을 경우 2 가지로
  • $$ 1C0 + 1C1 = 1 + 1 = 2 $$ 가 되고
  • 마지막으로 둘 다 안 입는 경우는 없으므로 -1을 하면
  • $$ 3 * 2 - 1 = 5 $$가 된다

다른 분의 코드

1
2
3
4
5
6
7
public int solution(String[][] clothes) {
return Arrays.stream(clothes)
.collect(groupingBy(p -> p[1], mapping(p -> p[0], counting())))
.values()
.stream()
.collect(reducing(1L, (x, y) -> x * (y + 1))).intValue() - 1;
}
  • 람다식 대박

결과

1번

번호 속도
테스트 1 통과 (0.93ms, 50.5MB)
테스트 2 통과 (0.85ms, 51.9MB)
테스트 3 통과 (0.86ms, 51.9MB)
테스트 4 통과 (0.93ms, 52.8MB)
테스트 5 통과 (0.80ms, 52MB)
테스트 6 통과 (0.88ms, 50.5MB)
테스트 7 통과 (0.84ms, 52.2MB)
테스트 8 통과 (0.85ms, 52.8MB)
테스트 9 통과 (0.86ms, 51.9MB)
테스트 10 통과 (0.87ms, 52.2MB)
테스트 11 통과 (0.89ms, 52.4MB)
테스트 12 통과 (0.82ms, 51.9MB)
테스트 13 통과 (0.81ms, 52.3MB)
테스트 14 통과 (0.75ms, 51.9MB)
테스트 15 통과 (0.82ms, 50.5MB)
테스트 16 통과 (0.91ms, 50.5MB)
테스트 17 통과 (0.90ms, 52.5MB)
테스트 18 통과 (0.88ms, 52.1MB)
테스트 19 통과 (0.88ms, 51.8MB)
테스트 20 통과 (0.90ms, 52.4MB)
테스트 21 통과 (0.80ms, 52.9MB)
테스트 22 통과 (0.86ms, 51.6MB)
테스트 23 통과 (0.80ms, 50.7MB)
테스트 24 통과 (0.90ms, 50.3MB)
테스트 25 통과 (0.90ms, 52.3MB)
테스트 26 통과 (0.93ms, 52.1MB)
테스트 27 통과 (0.86ms, 52.3MB)
테스트 28 통과 (0.90ms, 50.1MB)

2번

번호 속도
테스트 1 통과 (14.41ms, 53.2MB)
테스트 2 통과 (12.19ms, 52.8MB)
테스트 3 통과 (12.05ms, 52.9MB)
테스트 4 통과 (13.03ms, 50.9MB)
테스트 5 통과 (14.21ms, 50.9MB)
테스트 6 통과 (12.85ms, 51MB)
테스트 7 통과 (13.19ms, 50.9MB)
테스트 8 통과 (13.32ms, 53.6MB)
테스트 9 통과 (13.37ms, 53.1MB)
테스트 10 통과 (12.58ms, 53.2MB)
테스트 11 통과 (12.34ms, 52.7MB)
테스트 12 통과 (13.69ms, 52.4MB)
테스트 13 통과 (13.19ms, 52.7MB)
테스트 14 통과 (12.48ms, 51.1MB)
테스트 15 통과 (11.82ms, 52.5MB)
테스트 16 통과 (12.63ms, 52.7MB)
테스트 17 통과 (13.37ms, 53.1MB)
테스트 18 통과 (13.54ms, 53.1MB)
테스트 19 통과 (13.20ms, 50.7MB)
테스트 20 통과 (13.11ms, 52.7MB)
테스트 21 통과 (12.80ms, 52.6MB)
테스트 22 통과 (13.48ms, 53.2MB)
테스트 23 통과 (12.59ms, 52.5MB)
테스트 24 통과 (13.71ms, 50.9MB)
테스트 25 통과 (12.94ms, 52.4MB)
테스트 26 통과 (13.67ms, 50.9MB)
테스트 27 통과 (13.75ms, 53MB)
테스트 28 통과 (13.04ms, 51.7MB)

테스트 케이스

1
2
assertEquals(5, test.solution(new String[][] {{"yellow_hat", "headgear"}, {"blue_sunglasses", "eyewear"}, {"green_turban", "headgear"}}));
assertEquals(3, test.solution(new String[][] {{"crow_mask", "face"}, {"blue_sunglasses", "face"}, {"green_turban", "face"}}));

참고 사이트

댓글 공유

AOP란

  • AOP는 Ioc, DI, 서비스 추상화와 더불어 스프링의 3대 기반 기술
  • 선언적 트랜잭션 기능이 대표적

트랜잭션 코드의 분리

  • 비즈니스 로직 전 후에 로직과 전혀 상관없는 트랜잭션 경계를 설정해야 하기 때문에 필연적으로 코드가 지저분해짐

메서드 분리를 이용해 분리

  • 비즈니스 로직을 담당하는 코드만 따로 메서드로 추출
  • 하지만 여전히 비즈니스 로직이 아닌 보일 필요가 없는 트랜잭션 코드가 남아있음
  • 그렇다면 클래스에서 빼서 아예 눈에 안띄게 해버리자

DI를 이용한 클래스의 분리

  • DI의 기본 아이디어는 실제 사용할 오브젝트의 클래스 정체는 감춘 채 인터페이스를 통해 간접으로 접근하는 것

  • 그 덕분에 구현 클래스는 얼마든지 외부에서 변경 가능

  • UserService 인터페이스를 상속 받는 클래스를 2 가지로 나눔

    1. ServiceImpl(비즈니스 담당)
    2. ServiceTx(트랜잭션 담당)
  • Impl에선 비즈니스 로직을 담당하는 메서드만 남겨두고 Tx에선 userService를 구현한 다른 오브젝트를 주입 받고 트랜잭션 안에서 동작할 수 있도록 메서드 안에서 트랜잭션 경계를 설정

1
2
3
4
5
6
7
public void upgardeLevels() {
트랜잭션 스타트();

주입받은 userService를 구현한 오브젝트(Impl).upgrardeLevels();

commit() or rollback();
}

DI를 이용한 트랜잭션 경계설정 코드 분리의 장점

  1. 비즈니스 로직만 담긴 Impl 클래스에선 코드을 작성 시 트랜잭션 같은 비즈니스 로직과 관계 없는 부분에 대해 전혀 신경 쓰지 않아도 됌
  2. 테스트가 쉬워짐

다시 돌아와 AOP: 애스펙트(aspect) 지향 프로그래밍

  • 부가기능 모듈화 작업
  • 핵심기능에 부가되어 의미를 갖는 특별한 모듈
  • 에스펙트는 부가될 기능을 정의한 코드인 어드바이스, 어드바이스를 어디에 적용할지를 결정하는 __포인터컷__을 함께 갖고 있음
  • 위에서 트랜잭션 코드는 비즈니스 로직에 상관없는 기능인데 핵심기능에 침투해 들어가면서 설계와 코드가 모두 지저분해짐
  • 이런 부가기능 코든는 여기저기 메서드에 마구 흩어져서 나타나고 코드는 중복됌
  • 런타임 시에는 각 부가기능 에스펙트는 자기가 필요한 위치에 다이내믹하게 참여
  • 하지만 설계와 개발은 다른 특성을 띤 에스펙트들을 독립적인 관점으로 작성
  • AOP는 에스펙트를 분리함으로써 핵심기능을 설계하고 구현할 때 객체지향적인 가치를 지킬 수 있도록 도와주는 것
  • 애플리케이션을 특정한 관점을 기준으로 바라볼 수 있게 해준다는 의미에서 관점 지향 프로그래밍이라고도 함

프록시를 이용한 AOP

  • 스프링은 다양한 기술을 조합해 AOP를 지원하는데 가장 핵심은 프록시를 이용한다는 것
  • 프록시로 만들어서 DI로 연결된 빈 사이에 적용해 타깃의 메서드를 호출 과정에 참여 해서 부가기능을 제공
  • 스프링 AOP의 부가기능을 담은 어드바이스가 적용되는 대상은 오브젝트의 메서드

바이트코드 생성과 조작을 통한 AOP

  • AspectJ는 컴파일된 타깃의 클래스 파일 자체를 수정하거나 JVM에 로딩되는 시점을 가로채서 바이트코드를 조작하는 방식을 사용
  • 이 때 장점은,
    1. DI 컨테이너의 도움을 받아서 자동 프록시 생성방식을 사용하지 않아도 AOP 적용 가능하기 때문에 컨테이너가 없는 환경에서도 AOP의 적용 가능
    2. 프록시 방식보다 훨씬 강력하고 유연한 AOP가 가능, 프록시는 부가기능을 부여할 대상은 클라이언트가 호출 할 떄 사용하는 메서드로 제한되지만, 바이트코드를 직접 조작하기 떄문에 오브젝트의 생성, 필드 값으 조회와 조작, static 초기화등의 다양한 작업에 부가기능 부여 가능

AOP 용어 정리

  • 타깃
    • 부가기능을 부여할 대상
  • 어드바이스
    • 타깃에게 제공할 부가기능을 담을 모듈
  • 조인 포인트
    • 어드바이스가 적용될 수 잇는 위치
    • 스프링 AOP의 조인 포인트는 메서드의 실행단계
    • 타깃 오브젝트가 구현한 인터페이스의 모든 메서드는 조인 포인트가 됌
  • 포인트컷
    • 어드바이스를 적용할 조인 포인트를 선별하는 작업 또는 그 기능을 정의한 모듈
    • 메서드의 시그니처를 비교하는 방법을 주로 사용
  • 프록시
    • 클라이언트와 타깃 사이에 투명하게 존재하면서 부가기능을 제공하는 오브젝트
  • 어드바이저
    • 포인트컷과 어드바이스를 하나씩 갖고 있는 오브젝트
    • 어떤 부가기능(어드바이스)를 어디에(포인트 컷) 전달할 것인가를 알고 있는 AOP의 가장 기본이 되는 모듈
  • 애스펙트
    • AOP의 기본 모듈
    • 한 개 또는 그 이상의 포인트 컷과 어드바이스를 조합
    • 보통 싱글톤 형태의 오브젝트

댓글 공유

문제

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


코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean solution(String[] phone_book) {
int length = phone_book.length;

for (int i = 0; i < length; i++) {
for (int j = 0; j < length; j++) {
if (i == j) {
continue;
}

if (phone_book[j].startsWith(phone_book[i])) {
return false;
}
}
}
boolean answer = true;
return answer;
}

흐름

  1. 배열을 전부 돌면서 해당 인덱스로 시장하는 단어가 있는 지 확인함


결과

번호 속도
테스트 1 통과 (0.78ms, 52.1MB)
테스트 2 통과 (0.52ms, 50.1MB)
테스트 3 통과 (0.65ms, 52.6MB)
테스트 4 통과 (0.51ms, 54.6MB)
테스트 5 통과 (0.55ms, 52.2MB)
테스트 6 통과 (0.66ms, 52.5MB)
테스트 7 통과 (0.87ms, 50.3MB)
테스트 8 통과 (0.77ms, 52.8MB)
테스트 9 통과 (0.64ms, 52.3MB)
테스트 10 통과 (0.57ms, 50.3MB)
테스트 11 통과 (0.65ms, 52.8MB)
  • 효율성 테스트
번호 속도
테스트 1 통과 (1.96ms, 60.3MB)
테스트 2 통과 (2.14ms, 59.8MB)

테스트 케이스

1
2
3
assertFalse(test.solution(new String[] {"119", "97674223", "1195524421"} ));
assertTrue(test.solution(new String[] {"123","456","789"} ));
assertFalse(test.solution(new String[] {"12","123","1235","567","88"} ));

여담

  • 사실 당연히 시간 초과로 안될 줄 알았는데 넘어가서 놀랐다.
  • 찾아보니 Trie 자료구조로 해결하길 바래서 카테고리가 해쉬 인 듯 싶다.
  • 하지만 쉽게 풀 수 있는 문제를 어렵게 풀 이유가 없으니 이 문제는 이렇게 마무리 하려한다.

댓글 공유

문제

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


코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int solution(int[] citations) {
int answer = 0;
int n = citations.length;

Arrays.sort(citations);

for (int i = 0; i <= citations[n - 1]; i++) {
int high = 0;

for (int j = 0; j < n; j++) {
if (i <= citations[j]) {
high++;
}

if (i <= high) {
answer = i;
}
}
}

return answer;
}

흐름

  1. 논문을 인용한 횟수가 담긴 배열 citations를 정렬

  2. 가장 많이 인용한 횟수 만큼 반복

  3. 0부터 배열 길이만큼 반복하면서 i 값이 인용 횟수보다 작은 경우 i 보다 인용된 횟수가 많은 것이므로 high 변수 증가

  4. h번 이상 인용된 논문이 h편 이상이여야 하므로 i가 high 보다 작을 경우에만 answer에 저장

  5. i가 high보다 커질 경우 answer에 저장되지 않으므로 answer에 마지막으로 저장된 값이 h


다른 해결방법

코드

1
2
3
4
5
6
7
8
9
10
11
public int solution(int[] citations) {
Arrays.sort(citations);

int max = 0;
for(int i = citations.length-1; i > -1; i--){
int min = (int)Math.min(citations[i], citations.length - i);
if(max < min) max = min;
}

return max;
}

위 코드의 로직

  1. 배열을 정렬 후 배열의 마지막 값과 1 부터 증가 시키면서 citations의 길이 만큼 반복함

  2. 예를 들어 {0, 1, 3, 5, 6} 인 경우, citations.length는 5, i = 5 - 1 이므로 4, citations.length - i는 5 - 4가 되므로 1임 그런고로, 1과 citations[4]인 6과 비교해서 작은 놈을 min에 저장

  3. 위 과정을 반복하면
    min = Min(6,1)
    min = Min(5,2)
    min = Min(3,3)
    min = Min(1,4)
    min = Min(0,5)
    가 되고 max는 max가 min 보다 작을 때 max에 min 값이 저장되므로 max엔 최종적으로 3이 저장

  • 어떻게 유추해서 푼 건지 이해가 안간다 천재인 듯

결과

1번

번호 속도
테스트 1 통과 (11.43ms, 50.3MB)
테스트 2 통과 (14.71ms, 52.4MB)
테스트 3 통과 (15.13ms, 50.7MB)
테스트 4 통과 (13.74ms, 50.1MB)
테스트 5 통과 (16.49ms, 50.6MB)
테스트 6 통과 (13.86ms, 50MB)
테스트 7 통과 (10.08ms, 52.4MB)
테스트 8 통과 (7.45ms, 52.6MB)
테스트 9 통과 (8.26ms, 52.9MB)
테스트 10 통과 (10.29ms, 50MB)
테스트 11 통과 (17.04ms, 50.9MB)
테스트 12 통과 (8.21ms, 52.8MB)
테스트 13 통과 (15.30ms, 52.5MB)
테스트 14 통과 (13.62ms, 52.7MB)
테스트 15 통과 (15.64ms, 50.5MB)
테스트 16 통과 (0.97ms, 54.7MB)

2번

번호 속도
테스트 1 통과 (1.10ms, 52.4MB)
테스트 2 통과 (1.36ms, 50.5MB)
테스트 3 통과 (1.36ms, 50.8MB)
테스트 4 통과 (0.85ms, 51.9MB)
테스트 5 통과 (0.95ms, 52.5MB)
테스트 6 통과 (1.35ms, 52.7MB)
테스트 7 통과 (1.00ms, 50.6MB)
테스트 8 통과 (0.87ms, 50.9MB)
테스트 9 통과 (1.06ms, 50.5MB)
테스트 10 통과 (1.11ms, 52.6MB)
테스트 11 통과 (1.39ms, 52.5MB)
테스트 12 통과 (0.91ms, 52.6MB)
테스트 13 통과 (1.22ms, 50.3MB)
테스트 14 통과 (1.50ms, 52.5MB)
테스트 15 통과 (1.36ms, 52.3MB)
테스트 16 통과 (0.94ms, 52.5MB)
  • 속도도 압도적…

테스트 케이스

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

댓글 공유

문제

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


코드

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 String solution(int[] numbers) {

List<Numbers> list = new ArrayList<Numbers>();
for (int i = 0; i < numbers.length; i++) {
int number = numbers[i];

char[] str = String.valueOf(number).toCharArray();
StringBuilder fourDigitString = new StringBuilder();

fourDigitString.append(str);
if (str.length < 4) {

for (int j = 0; j < 4 - str.length; j++) {
fourDigitString.append(str[((j) % str.length)]);
}
}

list.add(new Numbers(i, Integer.parseInt(fourDigitString.toString())));
}

Collections.sort(list, new Comparator<Numbers>() {

@Override
public int compare(Numbers o1, Numbers o2) {
return o2.value.compareTo(o1.value);
}
});

StringBuilder sb = new StringBuilder();
int sum = 0;
for (Numbers n : list) {
sum += n.value;
sb.append(numbers[n.postion]);
}

if (sum == 0) {
sb.setLength(0);
sb.append(0);
}

return sb.toString();
}

class Numbers {
public Integer postion;
public Integer value;

public Numbers(Integer postion, Integer value) {
this.postion = postion;
this.value = value;
}
}

흐름

  1. 문제에서 numbers의 원소 값이 1000 이하라고 했으므로 numbers 크기 만큼 돌면서 numbers 안에 값이 4자리가 아닌 경우 자기 숫자를 뒤에 덧붙여서 4자리로 만듬
    • ex) 121 > 1212, 1 > 1111, 23 > 2323, …
  2. 현재 배열의 index와 값을 비교하기 위에 inner class를 하나 만들어서 index와 value를 저장하고 class를 list에 추가
  3. list를 class의 value 값으로 큰 수부터 나오도록 정렬
  4. list를 돌면서 value를 StringBuilder로 붙임
  5. 이 때, 0 0 0 0 일 경우 0 이 되어야 하므로 value들을 모두 더했을 때 값이 0이라면 0 만 붙임
  6. toString() 으로 String형으로 return

다른 분들의 해결방법

다른분의 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public String solution(int[] numbers) {
String answer = "";

List<Integer> list = new ArrayList<>();
for(int i = 0; i < numbers.length; i++) {
list.add(numbers[i]);
}
Collections.sort(list, (a, b) -> {
String as = String.valueOf(a), bs = String.valueOf(b);
return -Integer.compare(Integer.parseInt(as + bs), Integer.parseInt(bs + as));
});
StringBuilder sb = new StringBuilder();
for(Integer i : list) {
sb.append(i);
}
answer = sb.toString();
if(answer.charAt(0) == '0') {
return "0";
}else {
return answer;
}
}

위 코드의 로직

  • list에 numbers array를 그대로 저장하고 list의 저장된 element를 앞뒤로 붙여서 정수로 바꾼 후 비교해서 정렬
  • String을 다루는 부분이 많아서 속도는 살짝 떨어지는 듯

결과

1 번

번호 속도
테스트 1 통과 (139.80ms, 83.2MB)
테스트 2 통과 (88.71ms, 67MB)
테스트 3 통과 (167.96ms, 82.4MB)
테스트 4 통과 (17.95ms, 50.7MB)
테스트 5 통과 (129.09ms, 77.4MB)
테스트 6 통과 (116.84ms, 71.2MB)
테스트 7 통과 (1.44ms, 52.3MB)
테스트 8 통과 (1.38ms, 52.7MB)
테스트 9 통과 (1.20ms, 50.2MB)
테스트 10 통과 (1.46ms, 52.6MB)
테스트 11 통과 (1.65ms, 52.9MB)

2 번

번호 속도
테스트 1 통과 (340.75ms, 105MB)
테스트 2 통과 (236.67ms, 82.1MB)
테스트 3 통과 (350.56ms, 126MB)
테스트 4 통과 (80.72ms, 59.7MB)
테스트 5 통과 (300.18ms, 110MB)
테스트 6 통과 (280.61ms, 87.6MB)
테스트 7 통과 (31.26ms, 57.2MB)
테스트 8 통과 (35.88ms, 53.4MB)
테스트 9 통과 (34.27ms, 55.3MB)
테스트 10 통과 (30.44ms, 55.6MB)
테스트 11 통과 (40.16ms, 55.3MB)

테스트 케이스

1
2
3
4
5
6
7
8
9
assertEquals("6210", test.solution(new int[] {6, 10, 2}));
assertEquals("9534330", test.solution(new int[] {3, 30, 34, 5, 9}));
assertEquals("220200", test.solution(new int[] {2,200,20}));
assertEquals("2200", test.solution(new int[] {2,0,20}));
assertEquals("0", test.solution(new int[] {0,0,0}));
assertEquals("21212", test.solution(new int[] {12, 212}));
assertEquals("21221", test.solution(new int[] {212, 21}));
assertEquals("7000", test.solution(new int[] {0, 0, 70}));
assertEquals("1000000", test.solution(new int[] {0, 0, 0, 1000}));

댓글 공유

문제

![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"));

참고 사이트

댓글 공유

Junggu Ji

author.bio


author.job