[프로그래머스] 약수의 개수와 덧셈

🔗 출처

약수의 개수와 덧셈 : https://programmers.co.kr/learn/courses/30/lessons/77884

📔 문제설명

두 정수 leftright가 매개변수로 주어집니다. left부터 right까지의 모든 수들 중에서, 약수의 개수가 짝수인 수는 더하고, 약수의 개수가 홀수인 수는 뺀 수를 return 하도록 solution 함수를 완성해주세요.

✅ 제한사항

1 ≤ left ≤ right ≤ 1,000

🔍 입출력 예

left right result
13 17 43
24 27 52

📝 풀이

어떤 자연수의 약수를 구하는 가장 쉬운 방법은 자연수 N을 i = 1 ~ N 까지 나눠서 나머지가 0으로 나오는 i의 개수를 찾으면 된다.

이러한 경우 최소값 1 ~ 자기자신 N까지 확인하므로 시간복잡도는 O(n) 이 나온다.

더 빠르게

여기서 약수의 특성에 대해서 조금 더 생각해 본다면 항상 약수는 그 짝이 되는 수가 존재한다. (ex. 15 = 3 * 5)

즉, N의 약수들 중 두 약수의 곱이 N이 되는 약수 a,b는 반드시 존재하므로 N의 제곱근까지 약수를 구하면 그 짝이 되는 약수는 자동으로 구했다고 볼 수 있다.

이 방법을 사용하여 약수를 구하면 시간복잡도는 O(n^(1/2)) 이 나온다.

➕ 추가

프로그래머스에서 올린 해설을 찾아보니 애초에 문제에서 요구하는건 모든 약수를 구하는게 아니라 약수의 개수가 짝수인건 더하고 홀수인건 빼는 것이라서 약수가 홀수인지 짝수인지만 구하면 된다.

약수를 구해보면 약수가 홀수라면 그 수는 약수의 제곱수로 나오므로 매개변수 left ~ right 의 제곱수만 구하면 문제를 풀 수 있다.

[프로그래머스] 신규 아이디 추천

🔗 출처

신규 아이디 추천 : https://programmers.co.kr/learn/courses/30/lessons/72410

📔 문제 설명

카카오에 입사한 신입 개발자 네오는 "카카오계정개발팀"에 배치되어, 카카오 서비스에 가입하는 유저들의 아이디를 생성하는 업무를 담당하게 되었습니다. "네오"에게 주어진 첫 업무는 새로 가입하는 유저들이 카카오 아이디 규칙에 맞지 않는 아이디를 입력했을 때, 입력된 아이디와 유사하면서 규칙에 맞는 아이디를 추천해주는 프로그램을 개발하는 것입니다.
다음은 카카오 아이디의 규칙입니다.

아이디의 길이는 3자 이상 15자 이하여야 합니다.
아이디는 알파벳 소문자, 숫자, 빼기(-), 밑줄(_), 마침표(.) 문자만 사용할 수 있습니다.
단, 마침표(.)는 처음과 끝에 사용할 수 없으며 또한 연속으로 사용할 수 없습니다.
"네오"는 다음과 같이 7단계의 순차적인 처리 과정을 통해 신규 유저가 입력한 아이디가 카카오 아이디 규칙에 맞는 지 검사하고 규칙에 맞지 않은 경우 규칙에 맞는 새로운 아이디를 추천해 주려고 합니다.
신규 유저가 입력한 아이디가 new_id 라고 한다면,

1단계 new_id의 모든 대문자를 대응되는 소문자로 치환합니다.
2단계 new_id에서 알파벳 소문자, 숫자, 빼기(-), 밑줄(_), 마침표(.)를 제외한 모든 문자를 제거합니다.
3단계 new_id에서 마침표(.)가 2번 이상 연속된 부분을 하나의 마침표(.)로 치환합니다.
4단계 new_id에서 마침표(.)가 처음이나 끝에 위치한다면 제거합니다.
5단계 new_id가 빈 문자열이라면, new_id에 "a"를 대입합니다.
6단계 new_id의 길이가 16자 이상이면, new_id의 첫 15개의 문자를 제외한 나머지 문자들을 모두 제거합니다.
만약 제거 후 마침표(.)가 new_id의 끝에 위치한다면 끝에 위치한 마침표(.) 문자를 제거합니다.
7단계 new_id의 길이가 2자 이하라면, new_id의 마지막 문자를 new_id의 길이가 3이 될 때까지 반복해서 끝에 붙입니다.

신규 유저가 입력한 아이디를 나타내는 new_id가 매개변수로 주어질 때, "네오"가 설계한 7단계의 처리 과정을 거친 후의 추천 아이디를 return 하도록 solution 함수를 완성해 주세요.

✅ 제한사항

new_id는 길이 1 이상 1,000 이하인 문자열입니다.
new_id는 알파벳 대문자, 알파벳 소문자, 숫자, 특수문자로 구성되어 있습니다.
new_id에 나타날 수 있는 특수문자는 -_.~!@#$%^&*()=+[{]}:?,<>/ 로 한정됩니다.

🔍 입출력 예

no new_id result
예1 "...!@BaT#*..y.abcdefghijklm" "bat.y.abcdefghi"
예2 "z-+.^." "z--"
예3 "=.=" "aaa"
예4 "123_.def" "123_.def"
예4 "abcdefghijklmn.p" "abcdefghijklmn"

📝 풀이

정규식을 사용하면 어렵지 않게 풀 수있는데 정규식을 자주 쓰진 않아서 자세한 규칙들은 블로그를 검색해보고 정규식 검증은 https://regexr.com에서 진행했다.

5.중간메모

복습 겸 다시 프로젝트 생성해서 만들어보다가 몇가지 간략히 정리

1. springboot에서 html파일을 templates에서 읽도록 하기

템플릿 엔진과 관련된 의존성을 추가하면 자동으로 /recourses/templates 아래에서 템플릿 파일을 찾도록 하는거 같은데 원래 기본 경로는 /recourses/static 이다.

따라서 별다른 설정을 하지 않고 html 파일을 templates 폴더 밑에 놓고 찾으려고 하면 당연히 404 에러를 볼수 밖에 없다.

html 파일을 templates 아래에 관리하고 싶다면 몇가지 작업을 해야하는데

우선 스프링부트에서 WebMVC 설정을 유지하면서 기능을 확장하기 위해 WebMvcConfigurerimplements 하고 addResourceHandlers 를 오버라이드하여 아래와 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/templates/")
.setCacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES));
}
}

2. application.properties -> application.yml 변환

가독성 측면에서 yaml 파일이 더 좋아보여서 기존 내용을 변환해서 쓰려다보니 naver oauth2 설정을 하면 오류가 발생한다.

redirect-uri: '{baseUrl}/{action}/oauth2/code/{registrationId}'

yaml 에서는 / (슬러시)를 그대로 쓰면 파싱 에러가 난다. 따옴표나 작은 따옴표로 감싸주면 된다.

JWT 개요 간단 정리

1. JWT(Json Web Token)란

JSON 객체를 사용해서 토큰 자체에 정보들을 저장하고 있는 Web Token 으로 가벼운 인증으로 사용이 쉽다. 일반적으로 클라이언트와 서버, 서비스와 서비스 사이 통신을 할 때 권한 인가(Authorization)을 위해서 사용된다.

2. 구조

  • Header : Signature를 해싱하기 위한 알고리즘 정보
1
2
3
4
{
"alg" : "HS256",
"typ" : "JWT"
}
  • Payload : 서버와 클라이언트가 주고받는, 시스템에서 실제로 사용될 정보에 대한 내용들
1
2
3
4
{
"name" : "jy",
"iat" : 1422779638
}
  • Signature : 토큰의 유효성 검증을 위한 문자열로 헤더와 페이로드를 합친 문자열을 서명한 값이다. 헤더의 alg에 정의된 알고리즘과 secret을 이용해 해싱하고 이 값을 다시 base64 인코딩하여 생성한다.
1
2
3
4
5
HMAC-SHA256(
secret,
base64urlEncoding(header) + '.' +
base64urlEncoding(payload)
)

이 세 부분은 각각 Base64 인코딩을 사용하여 점을 사용해서 연결되면 JWT가 완성되며 주로 HTTP 통신 시 Authorization key의 value 로 사용된다.

1
2
3
4
{
//Header Payload Signature
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiankiLCJpYXQiOjE0MjI3Nzk2Mzh9.SNKm-Pcut8DUMBmeQXdIJlM_wFkh4jYd5YtRT369JdI",
}

3. 장점과 단점

  • 장점

    • 중앙의 인증서버, 데이터 스토어에 대한 의존성이 없어서 시스템의 수평확장에 유리하다.
    • Base64 URL Safe Encoding을 사용하여 URL, Cookie, Header 어디에서든 모두 사용 가능하다.
  • 단점

    • Payload의 정보가 많아지면 네트워크 사용량이 증가하여 데이터 설계 고려 필요
    • 토큰이 클라이언트에 저장되서 서버에서 클라이언트의 토큰을 조작할 수 없다.

4.머스테치로 화면 구성하기

4. 머스테치로 화면 구성하기

4.1 서버 템플릿 엔진

템플릿 엔진이란, 지정된 템플릿 양식과 데이터가 합쳐져 HTML문서를 출력하는 소프트웨어를 이야기한다.

서버 템플릿 엔진을 이용한 화면 생성은 서버에서 Java 코드로 문자열을 만든 뒤 이 문자열을 HTML로 변환하여 브라우저로 전달한다.

반면 클라이언트 템플릿 엔진(Vue, React 등)을 이용한 SPA(Single Page Application)은 브라우저에서 화면을 생성한다. 즉, 서버에서 이미 코드가 벗어난 경우라서

서버에서는 Json 혹은 Xml 형식의 데이터만 전달하고 클라이언트에서 조립한다.

최근엔 리액트나 뷰와 같은 자바스크립트 프레임워크에서 서버사이드렌더링을 지원하는 모습을 볼 수 있지만 그건 나중에 생각하자.


머스테치

머스테치는 많은 언어를 지원하는 심플한 템플릿 엔진이다.

스프링 부트에서 공식 지원하는 템플릿 엔진으로 gradle에 의존성 한줄 추가하면 바로 사용할 수 있다.

파일위치는 기본적으로 src/main/resources/templates이며 이 위치에 머스테치 파일을 두면 스프링 부트에서 자동으로 로딩한다.

해당 위치에 index.mustache를 생성한 후 이 머스테치에 URL을 매핑하는데 이는 Controller에서 진행한다.

  • IndexController
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;


@Controller
public class IndexController {

@GetMapping("/")
public String index() {
return "index";
}
}

머스테치 스타터 의존성을 추가했기 때문에 컨트롤러에서 문자열을 반환할 때 앞의 경로와 뒤의 파일 확장자는 자동으로 지정된다.

즉, 여기선 "index"를 반환하므로 src/main/resources/templates/index.mustache로 전환되어 View Resolver가 처리하게 된다.

(View Resolver는 URL 요청의 결과를 전달할 타입과 값을 지정하는 관리자 격으로 볼 수 있다.)


화면 구성 시 bootstrap을 사용하는데 공통된 부분에 대해선 layout을 따로 둬서 header와 footer 파일을 각각 만들어서 공통된 코드는 해당 위치에 생성한다.

여기서 페이지 로딩속도를 높이기 위해 css는 header에, js는 footer에 두는데 HTML은 위에서부터 코드가 실행되기 때문에 head가 다 실행되고서야 body가 실행된다.

즉, head가 다 불러지지 않으면 사용자 쪽에선 백지 화면만 노출되며 특히 js의 용량이 크면 클수록 body 부분의 실행이 늦어지기 때문에 js는 body 하단에 두어 화면이 다 그려진 뒤에 호출하는 것이 좋다.

header와 footer를 index에 추가하는건 아래와 같다.

1
2
3
4
5
6
7
8
9
10
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹 서비스</h1>
<div class="col-md-12">
<div class="row">
<div class="col-md-6">
<a href="/posts/save" role="button" clas="btn btn-primary">글 등록</a>
</div>
</div>
</div>
{{>layout/footer}}

{{> }} 는 현재 머스테치 파일을 기준으로 다른 파일을 가져온다.

화면의 버튼에 API를 호출하는 js파일을 작성하여 footer.mustache에 추가한다.

1
2
3
4
5
6
7
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>

<!--index.js 추가-->
<script src="/js/app/index.js"></script>
</body>
</html>

index.js 호출 코드는 절대경로(/)로 바로 시작하는데 스프링 부트는 기본적으로 src/main/resources/static에 위치한 자바스크립트, CSS, 이미지 등 정적 파일들은 URL에서 / 로 설정된다.


  • PostsRepository 인터페이스
1
2
3
4
5
6
public interface PostsRepository extends JpaRepository<Posts, Long> {

@Query("SELECT p FROM Posts p ORDER BY p.id DESC")
List<Posts> findAllDesc();
}

@Query 어노테이션을 사용하면 SpringDataJpa에서 제공하지 않는 메소드를 쿼리로 직접 작성할 수 있다.

보통 규모가 있는 프로젝트에선 데이터 조회는 FK의 조인, 복잡한 조건 등으로 인해 Entity 클래스만으로 처리가 어려워 조회용 프레임워크를 추가로 사용한다.

대표적 예로 querydsl, jooq, MyBatis 등이 있는데 해당 프레임워크 중 하나로 조회를 하고 그 외 등록/수정/삭제 등은 SpringDataJpa를 통해 진행한다.

  • PostsService
1
2
3
4
5
6
...
@Transactional(readOnly = true)
public List<PostsListResponseDto> findAllDesc() {
return postsRepository.findAllDesc().stream().map(PostsListResponseDto::new).collect(Collectors.toList());
}
...

@Transactional 어노테이션에 추가된 readOnly 옵션을 true로 주면 트랜잭션 범위는 유지하되, 조회기능만 남겨두어 조회 속도가 개선되기 때문에 등록, 수정, 삭제 기능이 전혀 없는 서비스 메소드에서 사용하는 것을 추천한다.


  • IndexController
1
2
3
4
5
6
7
8
9
10
11
12
@RequiredArgsConstructor
@Controller
public class IndexController {

private final PostsService postsService;

@GetMapping("/")
public String index(Model model) {
model.addAttribute("posts", postsService.findAllDesc());
return "index";
}
}

Model

  • 서버 템플릿 엔진에서 사용할 수 있는 객체를 저장할 수 있다.
  • 여기서는 postsService.findAllDesc()로 가져온 결과를 posts로 index.mustache에 전달한다.

REST에서 CURD는 다음과 같이 HTTP Method에 매핑된다.

  • 생성(Create) : POST
  • 읽기(Read) : GET
  • 수정(Update) : PUT
  • 삭제(Delete) : DELETE