요즘 그렇게 핫하다는 (사실 예전부터 핫했지만 내가 이제 알았다)

파이썬 프레임워크 FastAPI를 찍먹해보려한다.

 

전부터 공부하고 싶었던 분야가

1. NoSQL Database (MongoDB)

2. 마이크로서비스

였는데 FastAPI로 프로젝트를 준비하면서 같이 찍먹해보려한다.

 

일단 FastAPI 찍먹을 해볼겸 간단한 프로젝트를 하면서 몽고디비 공부를 같이하고

나중에 이 프로젝트에 이것 저것 추가할 기능이 생긴다면 자연스럽게 마이크로서비스 공부로 이어가면 되지 않을까? 싶다.

 

 

개발환경 설정

사지방에서 공부를 하고 있기 때문에 Goorm IDE를 사용했다.

 

1. MongoDB 설치

Goorm IDE에서 컨테이너를 생성할때 기본 옵션으로 MongoDB 설치가 가능하다.

MongoDB 설치 체크박스를 클릭하고 컨테이너를 생성하자.

만약 못 했다면 아래 주소를 참고하자.

https://help.goorm.io/ko/goormide/18.faq/language-and-environment/how-to-install-mongodb

 

MongoDB를 설치하고 싶습니다 - goorm

apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4 && (echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/4.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.0.list) &&

help.goorm.io

2. 가상환경 생성 및 fastapi 설치

mkdir venvs
cd venvs
python3 -m venv venv_for_fastapi #가상환경 생성

cd venv_for_fastapi/bin
source activate # 가상환경 실행

python venv 를 이용해 가상환경을 설치해줬다.

source activate를 이용해 가상환경을 실행시키자.

pip install fastapi
pip install uvicorn
pip install --upgrade pip

가상환경 실행 후 fastapi와 uvicorn을 설치해준다.

(fastapi 만으론 웹 개발이 불가능, uvicorn을 이용해 서버를 실행시키고 배포한다.)

 

3. 실행 url 및 포트 관리

좌측 상단 > 프로젝트 > 실행 ur과 포트 항목에서 원하는 url을 설정하고 포트 번호를 8000(fastapi 기본 포트)로 등록한후 확인을 누른다.

그리고 나온 url을 복사해둔다.

4. main.py 작성 및 테스트

#main.py
from fastapi import FastAPI

app = FastAPI() 

@app.get("/") 
def test():
    return {"message" : "Hello World"}

main.py 파일을 만든 이후 다음과 같이 작성해본다.

이후 터미널에서 아래 명령어를 입력한다.

uvicorn main:app --reload #기본 명령어
uvicorn main:app --reload --host=0.0.0.0 --port=8000 #goorm ide로 실행할때

그 후 3번에서 복사한 url로 접속해보면

hello world가 띄워져 있는 것을 확인 할 수 있다.

 

 

'FastAPI' 카테고리의 다른 글

사지방에서 FastAPI 공부하기 #2 - 프로젝트 구조  (1) 2023.04.01

Python 과 C++를 이미 배운 입장에서 정리해봤습니다.

 

Hello world !

public class Main {
	public static void main(String[] args){
		System.out.println("Hello World!");
	}
}

시작부터 일단 뭐가 엄청 길다

파이썬은 print("Hello World!") 가 끝이었는데

 

1. 자료형

원시(Primitive) 자료형

JAVA는 Python과 C++ 의 특징을 둘 다 가지고 있다.

포인터가 존재하지 않고 메모리 관리를 직접 하지 않아도 되는 점은 Python과 같지만

변수나 함수의 반환 자료형을 선언할때 지정해줘야 하는 점은 C++과 같다.

public class Sample {
	int a = 1;
	char = 't';
	float c = 3.0;
	double d = 4.0;
	boolean e = true;
    
	int[] array = {1,2,3,4,5};
}

그러나 C++과 약간의 차이점이 있다.

bool -> boolean

Wrapper class의 존재

 

그 외의 대부분은 C++과 매우 유사하며

문자열 포맷팅 또한 C++ 처럼 %d, %c, %f 개행문자를 사용한다.

단 String.format()또는 System.out.printf()를 사용해야한다.

 

배열 선언 또안 C와 동일하게 []를 통해 선언한다.

 

String

public class Main {
    public static void main(String[] args){
        String myString1 = "hello world";
        String myString2 = "hello";
    
        System.out.println(myString1.equals(myString2)); // return false
        System.out.println(myString1.indexOf('h')); // return 0
        System.out.println(myString1.contains(myString2)); // return true
        System.out.println(myString1.charAt(0)); // return 'h'
        System.out.println(myString1.replaceAll("world","java")); // return hello java
        System.out.println(myString1.substring(0,4)); // return hello
        System.out.println(myString1.equals(myString2)); // false
    }
}

C++의 string이 JAVA에서도 String 으로 제공된다.

다만 따로 import(include) 하지 않다고 사용할 수 있다.

 

List

import java.util.ArrayList;

public class Main {
    public static void main(String[] args){
        ArrayList myArray = new ArrayList();
        myArray.add("123");
        myArray.add("456");
        
        System.out.println(myArray.get(0)); // "123"
        System.out.println(myArray.get(1)); // "456"
    }
}

 

list는 주로 ArrayList를 이용한다.

C++과는 다르게 add,get 메소드를 이용해 정보를 입력하고 조회한다.

import java.util.Comparator;
import java.util.Arrays;
import java.util.ArrayList;

public class Main {
    public static void main(String[] args){
        String [] myArray1 = {"123", "456"};
        ArrayList <String> myArray2 = new ArrayList <> (Arrays.asList(myArray1));
        
        System.out.println(myArray2.get(0)); // "123"
        System.out.println(myArray2.get(1)); // "456"
        
        System.out.println(myArray2.size()); // "2
        System.out.println(String.join("", myArray2)); // "123456"
        
        myArray2.sort(Comparator.naturalOrder());
        System.out.println(myArray2); // ["123","456"]
        
        myArray2.sort(Comparator.reverseOrder());
        System.out.println(myArray2); // ["456", "123"]
    }
}

또 <> 제네릭스(Generics) 를 이용해 리스트에 들어갈 자료형을 선언해 줄 수 있다.

size() join() sort() 등의 함수도 사용할 수 있으나 사용법은 C++과 조금씩 차이가 있다.

Map

import java.util.HashMap;

public class Main {
    public static void main(String[] args){
        HashMap <String,String> myMap = new HashMap <> ();
        
        myMap.put("key1","value1");
        myMap.put("key2","value2");
        System.out.println(myMap.get("key1")); // "value1"
        System.out.println(myMap.containsKey("key2")); // true
        System.out.println(myMap.keySet()); //[key1,key2]
        System.out.println(myMap.remove("key2")); //value2
        System.out.println(myMap.size()); // 1
    }
}

HashMap을 통해 Map을 구현 가능하다.

C++ 의 map과 거의 동일하다.

put,get,remove를 통해 삽입,조회,삭제를 할 수 있다.

containsKey를 통해 키 유무를 확인할 수 있고 size를 통해 크기를 알 수 있다.

Set

package project;

import java.util.HashSet;
import java.util.Arrays;

public class Main {
    public static void main(String[] args){
        HashSet <String> mySet1 = new HashSet <> (Arrays.asList("1","2","3"));
        HashSet <String> mySet2 = new HashSet <> (Arrays.asList("3","4","5"));
        
        System.out.println(mySet1); // [1,2,3]
        
        mySet1.retainAll(mySet2); // 교집합
        System.out.println(mySet1); // [3]
        
        mySet1.addAll(mySet2); // 합집합
        System.out.println(mySet1); // [3,4,5]
        
        mySet1.removeAll(mySet2); // 여집합
        System.out.println(mySet1); // []
    }
}

HashSet를 통해 Set를 구현이 가능하다.

retailAll,addAll,removeAll을 통해 교집합,합집합,여집합 연산을 수행할 수 있다.

제어문 (if , if else, else, for, while, switch 등등)

제어문 구조는 c와 그냥 일치한다.

c 문법대로 그대로 사용하면 될 듯 하다.

 

2. Class

생성

package project;

class Animal {
    int legs = 4;
}
class Cat extends Animal {
    int getLegs(){
        return this.legs;
    }
}
class Dog extends Animal {
    String name;
    Dog(String name){
        this.name = name;
    }
    int getLegs(){
        return this.legs;
    }
    String getName(){
        return this.name;
    }
}

public class Main {
    public static void main(String[] args){
        Dog dog = new Dog("puppy"); // 클래스 생성
        Animal cat = new Cat(); // 업캐스팅
        //Dog animal = new Animal(); // 에러! 다운캐스팅은 불가능!!
        // System.out.println(cat.getLegs()); //에러!
        System.out.println(dog.getLegs()); // 4
        System.out.println(dog.getName()); // puppy
    }
}

객체 생성 방법은 C++에서 new를 이용해 동적으로 객체를 생성하는 방법과 동일하다.

상속 키워드는 extends 이며 C++과 다르게 다중 상속은 지원하지 않는다.

메소드 오버라이딩 오버로딩 모두 가능하며 업캐스팅 생성은 가능하지만 다운캐스팅은 불가능하다.

생성자는 C++과 동일하게 반환값 없이 클래스 이름을 그대로 사용한다.

Interface

C++에선 제공하지 않는 기능이다.

특정 기능을 구현할 것을 약속하는 추상 형식이다.

다형성 형성에 도움을 주며 다중상속도 가능하다.

 

https://wikidocs.net/217

 

05-07 인터페이스

인터페이스(Interface)는 초보 개발자를 괴롭히는 단골손님이다. 인터페이스에 대한 개념없이 코드로만 이해하려고 하면 곧 미궁에 빠지게 된다. 이렇게 이해하기 힘든 인터페이스…

wikidocs.net

추상(abstract) 클래스

C++의 virtual 클래스의 역할을 한다.

interface와 Class의 역할을 모두 할 수 있다.

 

제대로 공부하려면 연습문제나 간단한 프로젝트를 해보는 게 좋을 것 같다.

그건 다음편에 올려야겠다.

https://www.django-rest-framework.org/api-guide/testing/

 

Testing - Django REST framework

 

www.django-rest-framework.org

 

 

test code를 작성하는 이유?

1. 서버를 실행하고 DB에 데이터를 집어넣고 테스트 하는 등의 시간을 줄일 수 있다.

2. 테스트 코드를 먼저 작성하고 그에 맞춰 개발하는 방식으로 개발시간 단축, 깔끔한 코드 등의 이점을 얻을 수 있다 (TDD)

3. test를 진행할때 뿐 아니라 test 코드를 작성하는 과정에서도 미처 예상하지 못했던 오류를 찾을 수 있다.

4. test code 파일 자체가 api의 동작 방식과 결과에 대한 좋은 참고 문서가 될 수 있다.

5. 그냥 남들이 다 하길래

 

Django 및 DRF에서 test code를 작성하는 법

DRF는 unittest, TestCase,ApiReqeustFactory 등 여러 test code 작성을 위한 도구를 지원한다.

그 중 TestCase를 사용하기로 결정했다.

 

터미널에 python manage.py test를 입력하면

 

1. 장고는 프로그램속 TestCase를 상속받은 class를 찾아낸다.

2. 해당 클래스 내에서 setUp 과 "test"로 시작하는 메소드 함수를 찾아내 자동으로 실행한다.

3. 해당 함수 실행후 오류가 있는지 확인한 후 결과를 반환한다.

 

작성 예시

from django.test import TestCase

class MyTestCaseClass(TestCase): #TestCase 상속
    def setUp(self):
    	#setUp 데이터 생성
        
    def test_funtion(self):
        #테스트 하고 싶은 내용 입력

 

 

CocktailsURLTest class

from django.test import TestCase
from .models import *
import copy

test_cocktail_data = {
	"name" : "마이 타이",
    "base" : [
        {"name" : "다크 럼", "amount" : 30.0},
        {"name" : "골드 럼", "amount" : 30.0}
    ],
    "sub" : [
        {"name" : "오렌지 큐라소", "amount" : 15.0}
    ],
    "juice" : [
        {"name" : "라임 주스", "amount" : 30.0},
        {"name" : "오르쟈 시럽", "amount" : 15.0},
        {"name" : "심플 시럽", "amount" : 7.0}
    ],
    "other": [
        {"name" : "라임 필", "amount" : "1슬라이스"},
        {"name" : "민트 잎", "amount" : "3~4잎"}
    ],
    "recipe" : "위의 재료들을 ......... 완성.",
    "img_url": "https://t1.daumcdn.net/cfile/tistory/9923B0495D66434618",
    "glass" : "올드 패션드 글라스",
    "hashtag" : ["달달한","과일향이 나는"]
}

class CocktailsURLTest(TestCase):
    def setUp(self):
        Base.objects.create(name="다크 럼", alcohol_degree = 40.0)
        Base.objects.create(name="골드 럼", alcohol_degree = 40.0)
        Sub.objects.create(name = "오렌지 큐라소", alcohol_degree = 20.0)
        Juice.objects.create(name = "라임 주스")
        Juice.objects.create(name = "오르쟈 시럽")
        Juice.objects.create(name = "심플 시럽")
        Other.objects.create(name = "라임 필")
        Other.objects.create(name = "민트 잎")
        Glass.objects.create(name = "올드 패션드 글라스")
        HashTag.objects.create(name = "달달한")
        
    def test_api_cocktails_get(self):
        glass = Glass.objects.get(name = "올드 패션드 글라스")
        Cocktail.objects.create(name = "마이 타이", glass = glass)
        response = self.client.get('/cocktails')
        self.assertEqual(response.status_code,200)
        self.assertEqual(response.json()[0]["name"],"마이 타이")
        
    def test_api_cocktails_post(self):
        request_body = copy.deepcopy(test_cocktail_data)
        response = self.client.post('/cocktails',request_body,content_type = 'application/json')
        self.assertEqual(response.status_code,201)
        self.assertEqual(response.json()['name'],"마이 타이")

 

이 test code는 칵테일 레시피 사이트에서 레시피를 등록하고 조회하는 기능을 하는 /cocktails url을 테스트 하는 코드이다.

/cocktails url로 접속해 GET,POST method를 요청하면 DB에 있는 칵테일 레시피를 가져오거나, 새 레시피를 등록할 수 있다.

제대로 작동했을 경우 Django의 JsonResponse를 이용해 응답이 되돌아온다.

 

class CocktailsURLTest(TestCase):    
    def setUp(self):
        Base.objects.create(name="다크 럼", alcohol_degree = 40.0)
        Base.objects.create(name="골드 럼", alcohol_degree = 40.0)
        Sub.objects.create(name = "오렌지 큐라소", alcohol_degree = 20.0)
        Juice.objects.create(name = "라임 주스")
        Juice.objects.create(name = "오르쟈 시럽")
        Juice.objects.create(name = "심플 시럽")
        Other.objects.create(name = "라임 필")
        Other.objects.create(name = "민트 잎")
        Glass.objects.create(name = "올드 패션드 글라스")
        HashTag.objects.create(name = "달달한")

 

setUp 메소드 함수는 테스트가 시작되면 자동으로 실행된다.
앞으로 있을 test method 함수에서 사용될 데이터를 생성하는 용도로 사용한다.
필수는 아니며 생략 가능하다.

 

#views.py

@csrf_exempt
def cocktails(request):
    if request.method == 'GET': # 칵테일 레시피 전체 요청
        cocktails = Cocktail.objects.all()
        serializer = CocktailSerializer(cocktails, many=True)
        return JsonResponse(serializer.data,safe = False)
class CocktailsURLTest(TestCase):
	#.....
    
    def test_api_cocktails_get(self):
        glass = Glass.objects.get(name = "올드 패션드 글라스")
        Cocktail.objects.create(name = "마이 타이", glass = glass)
        response = self.client.get('/cocktails')
        self.assertEqual(response.status_code,200)
        self.assertEqual(response.json()[0]["name"],"마이 타이")

 

setUp 함수가 실행된 이후 "test" 라는 이름으로 시작하는 메소드 함수들이 실행된다.

self.client.get("my-url") 입력시 해당 url에 get 요청을 했을때 돌아오는 응답을 반환한다.

나는 JsonResponse 형태로 응답이 돌아오도록 views.py를 작성했다.

 

올바른 응답이 왔는지 확인하는 방법은 self.assert 함수를 이용하면 된다.

self.assertEqual은 함수 인자로 들어온 두 개의 값이 동일할 경우 테스트를 통과시키고 아닐경우 error를 반환한다.

assertEqual 뿐 아니라 assertTrue, assertFalse 등 여러가지 assert 함수가 존재하므로 상황에 맞게 사용하면 된다.

 

위 메소드 함수에선 assertEqual을 이용해

http status가 200인지, 돌아온 json의 "name" 항목이 "마이 타이"가 맞는지 확인한다.

 

 

TestCase 작성시 주의할 점

1. 파이썬은 TestCase를 상속받은 class에서 "test"로 시작하는 메소드함수만 테스트한다.

이걸 몰라서 처음에 왜 python manage.py test를 했는데 왜 아무것도 안일어나지 하고 당황했던 기억이 난다.

 

2. 메소드함수의 이름은 최대한 자세하게 지어야한다.

프로젝트의 크기가 커질수록 test code의 양도 굉장히 늘어난다.

또 test 실행 결과가 항상 잘 나오는 것은 아니다.

이때 수월한 디버깅을 위해선 함수 이름을 최대한 자세하게 지어야한다.

 

3. 하나의 메소드 함수에선 한개의 테스트만 진행한다.

마찬가지로 수월한 디버깅을 위해선 하나의 함수에서 하나의 경우의 수만 테스트해보는 것이 좋다.

 

 

 

 

 

 

 

 

 

 

 

 

 

프로젝트를 진행할때 git과 github를 이용해 관리를 하는 경우가 많다.

특히나 두 명이상 공동으로 진행하는 프로젝트라면 더더욱

 

git과 github의 차이가 뭔가요?

https://www.youtube.com/watch?v=YFNQwo7iTNc 

 

설명하기 귀찮으니까 더 잘 설명하는 사람의 영상과 글을 보도록 하자

https://backlog.com/git-tutorial/kr/

 

누구나 쉽게 이해할 수 있는 Git 입문~버전 관리를 완벽하게 이용해보자~ | Backlog

누구나 쉽게 알 수 있는 Git에 입문하신 것을 환영합니다. Git을 사용해 버전 관리를 할 수 있도록 함께 공부해봅시다!

backlog.com

 

그럼 git을 이용해서 어떻게 프로젝트를 관리한다는 건가요?

바로 branch라는 녀석을 이용하면 됩니다.

그리고 branch를 사용하는 방법도 여러가지가 있는데 이를 브랜치 전략이라고도 합니다.

 

git branch [브랜치 이름]     #로컬 브랜치 생성
git checkout [브랜치 이름]   #브랜치 이동
git remote add [origin] [레포지토리 주소] # 원격 브랜치 연결
git clone [레포지토리 주소]   #깃허브 리포지토리 복사

git add .*
git commit -m "커밋 메세지"
git push origin main

1. git-flow 전략

https://techblog.woowahan.com/2553/

 

우린 Git-flow를 사용하고 있어요 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요. 우아한형제들 배민프론트개발팀에서 안드로이드 앱 개발을 하고 있는 나동호입니다. 오늘은 저희 안드로이드 파트에서 사용하고 있는 Git 브랜치 전략을 소개하려고 합

techblog.woowahan.com

git flow 전략은 진행중인 프로젝트를 5가지의 브랜치로 나눠 관리한다.

 

1. main (master)  : 이미 완성된 출시가 가능한 브랜치

2. develop : 다음 버전을 개발하는 브랜치

3. feature : dev 브랜치에서 세부 기능을 개발하는 브랜치

4. release : 다음에 출시할 버전을 최종 준비하는 브랜치

5. hotfix : 출시 버전에서 발생한 버그를 긴급 수정 하는 브랜치

 

뭔가 기능을 추가하고 싶다면

1. main -> develop (현재 버전은 놔두고 개발을 진행할 브랜치 생성)

2. develop -> feature (세부 기능을 구현하는 브랜치 생성)

3. feature -> develop (각각 세부기능 구현 후 dev 브랜치에 merge)

4. develop -> release (release 브랜치 생성 후 다시 점검, 기능 추가 더 필요할 시 2,3 반복)

5. release -> main (출시해도 문제가 없을 시에 main 브랜치로 merge)

와 같은 과정을 겪는다.

 

2. github-flow 전략

git-flow의 복잡성이 github와는 어울리지 않다는 의견과 함께 나온 전략이다.

https://brownbears.tistory.com/603

 

[Git] 브랜치 전략 - Github flow

브랜치 전략이란? 브랜치 전략이란 여러 개발자가 1개의 저장소를 사용하는 환경에서 효과적으로 활용하기 위해 나온 개념입니다. 브랜치 생성, 병합 등의 git 구조를 활용해 보다 효율적으로 소

brownbears.tistory.com

gihub-flow에선 한 개의 main 만 관리한다.

 

뭔가 기능을 추가하고 싶다면 feature 브랜치 1개만 새로 추가하여 만든 후 다시 main 브랜치에 merge 하면 된다.

dev,release브랜치와 같은 준비단계없이 main 브랜치에 바로 merge를 하기 때문에 Pull Request에 신경을 써야한다.

 

3. gitlab -flow 전략

github-flow 전략은 너무 단순하다는 의견과 함께 나온 전략이다.

https://docs.gitlab.com/ee/topics/gitlab_flow.html

 

Introduction to GitLab Flow | GitLab

Documentation for GitLab Community Edition, GitLab Enterprise Edition, Omnibus GitLab, and GitLab Runner.

docs.gitlab.com

github flow에 존재하는 main - feature 브랜치 뿐 아니라

배포용 productions 브랜치와 production 브랜치에 올리기 전 테스트를 하거나 시간을 두고 반영하는 pre-production 브랜치가 추가됐다.

 

뭔가 기능을 추가하고 싶다면

1. github-flow처럼 main 브랜치에서 feature브랜치를 만들고 수정한다.

2. 출시 준비가 되었다면 pre-production 브랜치에서 통합 테스트 및 배포 전 시간을 가진다.

3. pre-production 브랜치에서 production 브랜치로 merge 한 후 출시한다.

 

 

사실 정해진 정답은 없다.

본인 프로젝트의 구조와 크기, 협업하는 인원에 따라 더 좋은 전략은 프로젝트 중간에라도 바뀔 수 있다.

칵테일 웹사이트 제작 중 오늘의 추천 칵테일이라는 기능을 구현해야 했다.

즉 24시간 마다 랜덤으로 칵테일 레시피 중 1개를 골라 프론트에 전달해줘야 했다.

 

매일 정각에 자동으로 구현되는 함수를 제작하기 위해 django-crontab 을 활용하기로 했다.

 

1. crontab 설치

pip install django-crontab

이후 django의 settings.py 파일로 진입해 INSTALLED_APPS 파일에 'django_crontab'을 추가해주면 된다.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    .
    .
    .
    .
    'django_crontab',
]

2. crontab 추가

첫번째로 crontab을 이용해 자동으로 수행할 함수를 작성해야 한다.

나는 cron.py 라는 파일을 따로 만든 후 함수를 작성했다.

# cron.py
from .models import Cocktail,TodayDrink

def todayDrink(): #오늘의 추천 칵테일 crontab 함수
    today_drink = Cocktail.objects.order_by("?").first()
    last_drink = TodayDrink.objects.all().first()
    
    if last_drink:
        last_drink.cocktail = today_drink
        last_drink.save()
    else:
        new_obj = TodayDrink.objects.create(cocktail = today_drink)
        new_obj.save()

그리고 다시 settings.py 파일로 진입해 맨 밑에 항목을 추가해준다.

CRONJOBS = [
    ('*/1 * * * *', 'api.cron.todayDrink')
]

 

앞의 */1 * * * * 은 시간을 세팅하는 항목이다.

다섯 개의 단위가 있다.
왼쪽부터 순서대로 분, 시, 일, 월, 요일(1-7, 월요일 - 일요일)이다.

*은 매 분, 시, 일, 월, 요일 을 뜻한다. -> ( * * * * *) 은 매 분마다 실행한다는 의미다.

* 대신 숫자를 넣으면 단위에 알맞는 숫자의 시각에 실행한다. -> (5 1 * * *) 매일 1시 5분에 실행한다는 뜻이다.

*/숫자는 간격을 의미한다. -> (*/5 * * * *) 5분마다 1번씩 실행한다는 뜻이다.

 

뒤에는 아까 설정한 함수가 있는 경로를 입력해주면 된다.

 

3. crontab 실행

터미널에 진입해 다음과 같은 명령어를 실행하면 된다.

sudo service cron start
python manage.py crontab add

 

REST API가 뭔가요?

라고 물으신다면

https://thankyou-ddabong-dochi.tistory.com/51

 

REST API 장고로 구현하기 - 1 (웹 기본지식)

API 란? Application Programming Interface 어플리케이션이 어떤 프로그램이 제공하는 기능을 사용할 수 있도록 연결하는 매개체 라고 하는데 잘 이해가 안된다. 대충 프로그램과 프로그램을 연결하는 통

thankyou-ddabong-dochi.tistory.com

예전에 써놓은 글이 있으니까 참고하시기 바랍니다.

 

아무튼 저 글 이후에 쓰던 글에서 만들던 칵테일 레시피 알려주는 사이트의 백엔드 api의 코드를 리팩토링하고 또 신기능을 추가해야 하는 상황이 생겼다.

 

저 글 쓰던 시절은 그냥 만들면서 배우자 라던 시절이었고, 또 웹사이트의 기능들이 떠오를때마다 즉시 새롭게 추가해주어야 상황이었기 때문에 지금와서 코드를 다시 읽어봤더니 아주 개판이었다.

 

오랫동안 웹 개발 공부를 놨었기 때문에 다시 재활치료도 할 겸 이 api 코드를 리팩토링하기로 결정했다.

 

1. 주요 기능 기획

다음과 같은 기능들이 필요하고 1~4는 이미 구현된 기능, 5,6은 앞으로 구현해야 할 기능이다.

1~4까지의 기능이 구현이 됐지만 아까 말했듯 코드 리팩토링 또한 해야했다.

 

2. DB Schema 설계

https://gitmind.com/kr/

 

GitMind - 무료 온라인 마인드 맵 도구

GitMind는 브레인 스토밍, 프로젝트 계획, 개발, 행동 및 기타 창의적인 작업을위한 무료 온라인 마인드 맵 메이커입니다. 이 도구를 사용하면 수많은 사용자와 마인드 맵을 공유하고 공동 작업 할

gitmind.com

구현해야 할 기능을 기획했으므로 GitMind를 이용해 DB Schema를 시각화해봤다.

물론 기획한 내용을 그대로 구현하진 않았지만 변수 이름 정도만 바뀌고 큰 구조는 동일하게 구현했다.

사실 db구조는 이미 만들어진 구조에서 몇가지만 수정해서 사용했다.

가장 큰 차이점은 JSONField의 삭제다.

 

예전 api에서 칵테일 재료와 양을 표현할때

{"잭 다니엘스" : 30.0}

과 같은 식으로 JSON을 보내고 받아야 했다.

이 과정에서 레시피 테이블에 join한 후 잭 다니엘스라는 재료 이름과, 30.0ml라는 재료양을 찾은 후

다시 dict형태로 직렬화 하고 주고 받는게 귀찮아서 json필드로 통채로 만들어서 저장하고 조회했다.

 

https://vixxcode.tistory.com/229

 

[Database][Postgresql] JSONField VS JOIN 에 대한 성능 체크(jsonb vs table)

공부하면서 json field을 쓰는 것을 보고 과연 join과 비교했을 때 얼마나 성능에 대한 이점이 있을까 고민을 하다가 한 번 실험해보기로 했습니다. framework: django database: postgresql 13 python version : 3.9 co

vixxcode.tistory.com

하지만 조회 과정에선 큰 성능 차이를 보이지 못했고 데이터 수정시 상당히 로직이 더 복잡해지는 결과를 초래해

그냥 Forigen Key를 이용해 두 테이블을 연결하는 방식으로 전환했다.

 

3. api 명세 기획

다음으론 API 명세서를 기획했다.

원래는 API 명세를 먼저 짜고 그에 맞춰서 개발을 하는 게 프론트와의 협업 과정에서 좋다.

그리고 이미 기존의 api 명세가 존재했다.

 

하지만 프론트도 코드 개편이 필요한 상황이므로 api 명세를 내 입맛대로 수정해도 된다고 OK 사인을 받았다.

사실 그래서 코드를 먼저 짜버리고 api 명세를 내가 편한대로 나중에 구현했는데

이게 상당히 큰 실수였다. (추후 설명 예정)

앞으론 시간을 좀 들여서라도 API 명세를 신중히 기획하고 거기에 맞춰서 구현하자.

 

4. 코드 구현

1) Models.py

from django.db import models

class Glass(models.Model): # 서빙 글라스 모델
    name = models.CharField(primary_key = True,max_length=255)

    def __str__(self):
        return self.name

class Cocktail(models.Model): # 칵테일 레시피 모델
    name = models.CharField(primary_key = True,max_length=255)
    recipe = models.TextField()
    img_url = models.URLField()
    glass = models.ForeignKey(Glass, on_delete=models.CASCADE, null=True, related_name = 'cocktail')
    
    def __str__(self):
        return self.name
    
class Base(models.Model): # Base 재료 모델
    name = models.CharField(primary_key = True,max_length=255)
    alcohol_degree = models.FloatField(default = 0.0)
    cocktail = models.ManyToManyField(Cocktail,through = 'CocktailBase',related_name = 'base')
    
class CocktailBase(models.Model): # Cocktail - Base 중간 테이블 모델
    cocktail = models.ForeignKey(Cocktail, on_delete=models.CASCADE, related_name = 'cocktail_base')
    name = models.ForeignKey(Base, on_delete=models.CASCADE, related_name = 'cocktail_base') # base-name
    amount = models.FloatField(default = 0.0)
    
# ...이하 생략 ...

Cocktail 모델에 Glass 와 Base 모델이 연결된 형태다.

이때 Cocktail - Glass 모델은 1:N 관계 이고, Cocktail - Base 모델은 M:N 관계이다.

장고에선 두 관계를 models.ForeignKey, 와 models.ManyToManyField를 이용해 구현한다.

 

구현할때 까다로웠던 점은 무슨 Base 재료가 들어가는지 뿐 아니라 해당 재료가 그 칵테일에서 몇 ml 정도 들어가는지도 조회할 수 있어야 한다.

 

블루 하와이완과 블루 사파이어를 예로 들어보자.

두 칵테일 모두 "말리부" 라는 재료가 들어간다.

하지만 블루 하와이안에는 말리부가 15ml, 사파이어에는 30ml가 들어간다고 가정해보자

이때 Cocktail의 "블루 하와이안" 이란 모델과 Base 의 "말리부" 라는 모델을 연결하고 또 "블루 사파이어"와 "말리부"를 연결한다.

그렇다면 15ml와 30ml라는 재료양의 정보는 어느 테이블에 저장하는것이 좋을까?

 

이렇게 두 모델을 연결하면서 추가적인 정보를 저장해야 할때는 중간 테이블 모델을 만들어 사용하면 된다.

CocktailBase라는 모델을 만든 후 다음과 같이 through 옵션을 이용해 사용하면 된다.

cocktail = models.ManyToManyField(Cocktail,through = 'CocktailBase',related_name = 'base')

2) serializers.py 구현

serializer가 뭔가요?

라고 물으신다면

이 글을 읽고 오시는 걸 추천합니다.

https://thankyou-ddabong-dochi.tistory.com/53

 

REST API 장고로 구현하기 - 3 (연습 프로젝트 구현)

개발환경 Mac + Visual Studio Code + Python 3.10.5 django REST framework를 사용해 구현할 때 핵심은 Serializer 이다. django 내부의 복잡한 로직을 Serializer가 간단하게 구현해준다. 백엔드 API를 구현할 때 구현한

thankyou-ddabong-dochi.tistory.com

 

class BaseSerializer(serializers.ModelSerializer):
    class Meta:
        model = Base
        fields = ('name','alcohol_degree')      
        
class CocktailBaseSerializer(serializers.ModelSerializer):
    alcohol_degree = serializers.SerializerMethodField(read_only = True)
    class Meta:
        model = CocktailBase
        fields = ('name','amount','alcohol_degree')
        
    def get_alcohol_degree(self,obj):
        return obj.name.alcohol_degree

class CocktailSerializer(serializers.ModelSerializer): 
    glass = serializers.StringRelatedField(read_only = True)
    base = CocktailBaseSerializer(many = True,read_only = True, source ='cocktail_base')
    sub =  CocktailSubSerializer(many = True, read_only = True, source = 'cocktail_sub')
    juice = CocktailJuiceSerializer(many = True,read_only = True, source = 'cocktail_juice')
    other = CocktailOtherSerializer(many = True,read_only = True, source ='cocktail_other')

    class Meta:
        model = Cocktail
        fields = ('name','base','sub','juice','other','recipe','img_url','glass')

다음과 같은 방식으로 serializer를 구현했다.

DRF는 ManyToManyField를 직렬화하는 serializer를 구현할때

대부분 읽기 전용으로만 구현이 가능하고 생성시엔 create함수를 오버라이딩 하거나 views에서 직접 모델을 건드려야 한다.

그래서 나는 후자 방식을 선택했다.

CocktailSerializer의 필드를 보면 대부분 필드에 read_only = True로 설정되어 있는 것을 볼 수 있다.

 

그리고 읽을 때는 마찬가지로 중간 테이블의 정보를 같이 읽어야 하므로 CocktailBaseSerializer을 같이 만들어야 한다.

(이 방식 외에도 다른 방식이 있는데 DRF 공식 문서의 NestedSerializer 부분을 읽고 오는 것을 추천한다.)

가장 중요한 것은 source = 'cocktail_base'(중간 테이블 모델의 related_name 부분) 를 입력해주지 않으면

Serializer가 자동으로 중간테이블을 건너 뛰고 바로 Base 모델을 가져온다.

 

3) views.py

from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from .models import *
from .serializers import *

def check_cocktail_data(model,query_set): #유효성 검사 함수 - 재료가 존재하는지 확인
    for data in query_set:
        if not model.objects.filter(name = data['name']).exists():
            return False
    return True

# 중간 테이블에 칵테일과 재료 연결하는 함수
def create_mapping_table(mapping_table_model,query_set,cocktail,ingredient_model):
    for data in query_set:
        name = ingredient_model.objects.get(name = data['name'])
        mapping_table_model.objects.create(cocktail = cocktail, name = name, amount = data['amount'])

@csrf_exempt
def cocktails(request):
    if request.method == 'GET': # 칵테일 레시피 전체 요청
        cocktails = Cocktail.objects.all()
        serializer = CocktailSerializer(cocktails, many=True)
        return JsonResponse(serializer.data,safe = False)

    elif request.method == 'POST': # 칵테일 레시피 등록
        data = JSONParser().parse(request)
        
        base_data = data.pop('base') #외래키 등록 필요한 항목들
        sub_data = data.pop('sub')
        juice_data = data.pop('juice')
        other_data = data.pop('other')
        glass_data = data.pop('glass')

        serializer = CocktailSerializer(data = data) 
        # ---유효성 검사---
        if (serializer.is_valid()
        and Glass.objects.filter(name = glass_data).exists()
        and check_cocktail_data(Base,base_data)
        and check_cocktail_data(Sub,sub_data)
        and check_cocktail_data(Juice,juice_data)
        and check_cocktail_data(Other,other_data)):
            # --- 유효성 검사 통과 ---
            cocktail = serializer.save()
            
            glass = get_object_or_404(Glass, name = glass_data) #Glass 연결
            cocktail.glass = glass
            # --- 중간 테이블 연결 ---
            create_mapping_table(CocktailBase,base_data,cocktail,Base) 
            create_mapping_table(CocktailSub,sub_data,cocktail,Sub)
            create_mapping_table(CocktailJuice,juice_data,cocktail,Juice)
            create_mapping_table(CocktailOther,other_data,cocktail,Other)
            
            cocktail.save()
            return JsonResponse(serializer.data, status=status.HTTP_201_CREATED)
        
        # error_data = {"error_code":400, "error_message":f"invalid data"}
        return JsonResponse(serializer.errors, status = 400)

ManyToManyField 연결 을 views.py에서 진행했다.

POST method와 함께 request body로 데이터가 들어오면 올바른 데이터인지 유효성 검사를 진행한 후

외래키 테이블을 제외한 정보들(칵테일 이름, 조합법, 이미지 등)은 serializer을 통해 만들고

create_mapping_table함수를 통해 중간 테이블을 하나씩 연결해주는 방식이다.

 

5. 마무리하며

사실 일주일이라는 시간동안 리팩토링을 진행했는데 5일 정도는 시간을 날리기만 하고 진짜 개발은 마지막 2일에 다 한 것 같다.

평일에 컴퓨터를 할 수 있는 시간이 적어서 그런것도 같지만 그냥 api 명세를 제대로 안 짜고 맘대로 만들다가

이게 더 좋을 거 같은데..? 하고 다시 만들고.. 만들다가 막혀서 검색해봤더니 더 좋은 방식이 생각나서 또 갈아엎고..

이 과정을 반복하는데 5일을 썼다.

 

물론 그 과정에서 잊어버렸던 python 문법과 django, git, 노션 사용법, 에 관해서 다시 기억해낼 수 있는 시간이었다.

 

다음주 할 일

- 프론트와 상의 후 error_message 및 예외처리 구현

- 수정된 api 명세 검토

- 수정사항 main branch merge

- Hashtag, 및 좋아요 기능 구현 준비

 

 

 

 

입대한지 벌써 6개월차에 접어드는 지금 맨 처음 입대할때의 목표를 다시 살펴봤다.

1. 꾸준히 운동하기

2. 코딩 공부하기 ( 백준 플레찍기 )

3. 영어 공부하기

 

1은 하고 있고 2 번 목표는 생각보다 너무 빨리 달성해버렸다.

그렇다고 영어공부만 1년 3개월을 할 수는 없어서 알아보던 중 구름 ide를 이용해 사지방에서 웹 개발을 할 수 있다는 정보를 얻게 됐다.

그래서 입대하기 전에 만들던 칵테일 레시피 사이트의 백엔드 부분을 사지방에서 업데이트해보려고 한다.

 

입대 전 개발환경은

Mac + VS Code + Python3 + Django였다.

이제는 goorm ide 하나로 진행해보려고 한다.

 

1. Goorm IDE 회원가입 및 컨테이너 생성

https://ide.goorm.io/

 

구름IDE - 설치가 필요없는 통합개발환경 서비스

모두가 개발자가 된다 매일 전 세계 사용자들이 다양한 아이디어를 실현하고 있습니다. 프로젝트 멤버들과 실시간으로 공유하고 협업해 보세요. 지금 시작하기 코드 파일 수3,754,000+코드 작성

ide.goorm.io

그냥 들어가서 회원가입하고 컨테이너 생성을 누르면 된다.

이때 그냥 구글 아이디로 로그인을 하면 컴퓨터를 끄면 모든 내용이 초기화되는 사지방 특성상

매번 로그인 할 때마다핸드폰으로 Gmail을 키고 2차 인증을 해야하므로

이메일 인증을 하고 아이디를 새로 만드는 것을 추천한다.

 

 

컨테이너 생성시엔 장고를 이용해 개발을 한다고 소프트웨어 스택에서 냅다 장고를 고르지말고

 

파이썬으로 고르는 것을 추천한다.

장고를 고르고 했더니 알아서 장고 프로젝트를 만들어 주는건 좋은데

DRF를 이용해 백엔드 API를 만드려면 어차피 그 프로젝트 삭제하고 다시 만들어야한다.

 

프로젝트 이름 등은 알아서 설정하고

그 외에는 그냥 mysql 또는 mongoDB설치 정도만 설정하고 나머진 그냥 진행하면 될 것 같다.

 

2. 가상환경 설정 및 프레임워크 설치

소프트웨어 스택을 파이썬으로 설정했다면 파이썬3가 설치되어있는 ubuntu 기반의 서버를 사용할 수 있다.

root@goorm: /woorkspace/폴더 밑에 아까 설정한 프로젝트 이름으로 파일들이 생성된다.

mkdir venvs # 가상환경용 파일 생성
cd venvs # 만든 파일 진입
python -m venv cocktail # cocktail 이라는 이름으로 가상환경 생성

이후 만들어진 파일로 들어가 가상환경을 실행시키고 장고 및 필요한 프레임워크들을 다운 받으면 된다.

(django,django-rest-framework,mysqlclient,corsheaders 등을 설치했다.)

cd cocktail/bin
source activate #가상환경 실행

컨테이너 생성시에 mysql 설치 항목에 체크를 했다면 mysql은 이미 설치된 상태이다.

 

3. 깃허브 연동 및 프로젝트 파일 생성

장고 프로젝트 생성은 원래 "django-admin startproject" 명령어를 통해 진행하지만

기존에 만들던 백엔드 API를 수정하는 것이 목표이므로 깃허브에 미리 올려뒀던 파일을 clone 해오는걸로 대체했다.

왼쪽 탭의 GIT 메뉴를 누르고 저장소 연결하기를 통해 clone을 해도 되고

터미널에서 git clone "http:// ...." 명령어를 통해 clone을 해와도 된다.

 

4. DB연동 (MySQL)

mysql 설치를 이미 해놨기 때문에 장고 프로젝트에 연결만 하면 된다.

sudo service mysql start # mysql 시작
sudo mysql -u root -p #root 계정으로 mysql 진입 비밀번호는 아무거나 치면 됨
use mysql;
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY '원하는 비밀번호';

mysql이 제대로 설치됐는지 확인해본 후 루트 계정의 비밀번호를 바꿔준다. (phpmyadmin 사용을 위해)

https://retromakers.tistory.com/m/14

 

MySQL 초기설정 : DB 생성 및 신규 유저 권한 설정

MySql의 초기 설정 방법을 정리해 봤습니다. mysql이 설치되어 있고 mysql 관리자는 id와 pw를 알고 있는 경우를 가정합니다. DB와 이를 사용할 USER를 만들고, 내부/외부에서 접속이 가능하도록 권한 설

retromakers.tistory.com

이후 원하는 설정으로 유저 및 데이터베이스를 생성한다.

그리고 만든 유저 및 데이터베이스 설정대로 settings.py 파일로 들어가서 DATABASE 항목을 수정하면 된다.

#settings.py

#이 부분 지우기
# DATABASES = { 
#     'default': {
#         'ENGINE': 'django.db.backends.sqlite3',
#         'NAME': BASE_DIR / 'db.sqlite3',
#     }
# }

#새로 쓸 부분
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'db 이름',
        'USER': '사용자 이름',
        'PASSWORD': '비밀번호',
        'HOST': 'localhost',
        'PORT': '3306',
    }
}

그리고 python manage.py makemigrations, python manage.py migrate를 통해 마이그레이션을 진행해보고 이상이 있는지 확인해보면 된다.

 

https://incomeplus.tistory.com/224

 

Ubuntu MySQLClient 설치시 에러 발생 해결 OSError: mysql_config not found

명령어 $ sudo pip install mysqlclient Error OSError: mysql_config not found 원인 에러가 발생하는 이유는 mysqlclient 모듈을 설치하기 위한 종속성이 완성되지 않았기 때문이다. 즉, mysqlclient를 설치하기 전에 미

incomeplus.tistory.com

https://zenna9.tistory.com/43

 

django 실행 시 에러 해결(did you install mysqlclient?)

django를 실행하려고 python manage.py ....명령어들을 치면 자꾸 아래 메시지가 나왔습니다. django.core.exceptions.ImproperlyConfigured: Error loading MySQLdb module. Did you install mysqlclient? pip install mysqlclient를 하면 아

zenna9.tistory.com

mysqlclient가 설치되지 않았다는 오류가 생긴다면 이 페이지들을 참고하자.

 

마이그레이션이 잘 된다면 python manage.py runserver 명령어를 통해 장고 서버를 실행해보자.

 

5. 서버 외부 접속 테스트

프로젝트 > 실행 URL과 포트 메뉴를 이용해 서버 실행에 사용되는 도메인 주소를 등록할 수 있다.

원하는 도메인 아무거나 입력후 포트를 장고 기본 포트인 8000으로 설정하고 등록하자.

그러면 밑에 url 및 포트 주소와 복사하기 버튼이 생길텐데 그대로 복사하고 크롬으로 들어가서 붙여넣기하면

아까 실행한 장고 웹사이트가 나오는 것을 볼 수 있다.

 

6. DB GUI 설치 - phpMyAdmin

깃허브 관리는 터미널을 이용해 한다고 해도 db 관리를 터미널로 하기는 아직 쉽지 않다.

하지만 사지방 특성상 gui 프로그램을 설치할 수 없으므로(하모니카 OS로 바뀌고 파일 설치가 막힌 듯 하다)

설치 없이 돌릴 수 있는 phpMyAdmin 을 이용해 db를 관리하자.

 

https://wnw1005.tistory.com/535

 

우분투 20.04에서 phpMyAdmin 설치 01 - 저장소 설치

[관련 게시물] 2021.02.20 - [서버 운영/APM 서버 구축] - 우분투 20.04에서 phpMyAdmin 설치 01 - 저장소 설치 2021.03.15 - [서버 운영/APM 서버 구축] - 우분투 20.04에서 phpMyAdmin 설치 02 - Apache 서버 관리자를 위

wnw1005.tistory.com

 

이 게시글을 참고해서 설치했다.

이것 저것 오류가 많이 생겼었고 개발환경 설정하는데 여기서 시간을 제일 오래 잡아먹었는데

노션에 기록하는걸 까먹었다..

phpmyadmin 설치에 성공했다면 아까 했던 url 메뉴에서 80번 포트를 열어주면 된다.

그 후 80번 포트와 연결된 뒤에 /phpmyadmin을 입력한후 크롬 주소창에 넣으면 phpmyadmin 로그인 창이 나온다.

여기서 id에 root 그리고 비밀번호를 아까 설정한 root 계정 비밀번호를 입력하면 phpmyadmin을 이용해 웹으로 db관리를 할 수 있게 된다.

 

여기까지 성공한다면 이제 사지방에서 오로지 웹만 이용해 백엔드 개발을 할 수 있다.

 

API 테스트 - postman

DB GUI - phpMyAdmin

개발 - Goorm IDE

 

7. 개발환경 세팅 후 시작할 때

1. goorm ide 시작

2. 가상환경 진입

3. sudo service mysql start, sudo service apache2 start

4. phpmyadmin 실행

 

+ db를 초기화 해야하는 상황이 온다면

 

1. 장고 migrations 파일로 들어가 __init__.py를 제외한 모든 파일 삭제

2. sudo mysql -u root -p

3. drop database cocktail; (db 이름)

4. CREATE DATABASE cocktail;

5. 가상환경 진입 이후 python manage.py makemigrations, migrate

 

+ mysql utf8 오류 (db에 한글 삽입시 오류)

https://calvinjmkim.tistory.com/23

 

MySQL 데이터베이스의 characterset 언어를 UTF8로 변경하기

여전히 간단하게 사용하기에는 가장 좋은 관계형 데이터베이스는 MySQL인 것 같다. 그런데 MySQL에서 가장 흔히 하는 실수가 기본 언어셋을 변경하지 않는 것이다. 과거의 유물이어서 그런 탓이겠

calvinjmkim.tistory.com

이 과정대로 설정 변경후 mysql 재시작 및 db 재생성

 

 

 

 

 

 

최장 증가 부분 수열(LIS, Longest Increasing Subsequence) 알고리즘이란?

주어진 수열에서 오름차순으로 정렬된 가장 긴 부분수열을 찾는 문제이다. 이때 부분수열이 연속적이거나 중복되지 않아도 된다.

 

LIS 알고리즘은 크게 시간복잡도가 O(n^2) 인 알고리즘과 O(n logn) 인 알고리즘 두 가지로 구현 가능하다.

 

1. O(n^2) 알고리즘

https://www.acmicpc.net/problem/14002

 

14002번: 가장 긴 증가하는 부분 수열 4

수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오. 예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {10, 20, 10, 30, 20, 50} 이

www.acmicpc.net

n = int(input())
lst = list(map(int,input().split()))
dp = [0]*n
indexList = [-1]*n
    
dp[0] = 0
indexList[0] = 0
maxlen = 0

for i in range(1,n):
    for j in range(maxlen,-1,-1):
        if lst[i] > lst[dp[j]]:
            dp[j+1] = i
            indexList[i] = j+1
            if j == maxlen:
                maxlen += 1
            break
        elif j == 0:
            dp[0] = i
            indexList[i] = 0

print(maxlen+1)
stack = []

i = n-1
while maxlen >= 0 and i >= 0:
    if indexList[i] == maxlen:
        stack.append(lst[i])
        maxlen -= 1
    i -= 1
    
while stack:
    print(stack.pop(),end = " ")

DP를 응용해서 구현한다.

주어진 수열을 순차적으로 돌면서 DP 배열에 오름차순으로 숫자를 집어넣으면 된다.

이미 배열에 숫자가 있을 경우 오름차순 정렬을 해치지 않는 곳으로 원래있던 숫자를 대신해서 집어넣으면 된다.

그 후 index배열에 각 수열의 숫자가 들어간 dp 배열의 인덱스를 저장하면 된다.

 

예) 10 20 10 30 50 40 이 수열로 주어졌을 때

수열 1번째 수 10 -> DP 배열: [10] (0번 인덱스에 10 삽입) , indexList [0]

수열 2번째 수 20 -> DP 배열: [10, 20] (1번 인덱스에 20 삽입), indexList [0,1]

수열 3번째 수 10 -> DP 배열: [10, 20, 30] (2번 인덱스에 30 삽입), indexList [0,1,2]

수열 4번째 수 30 -> DP 배열: [10, 20, 30]( 0번 인덱스에 10 삽입), indexList [0,1,2,0]

수열 5번째 수 50 -> DP 배열: [10, 20, 30, 50] (3번 인덱스에 50 삽입), indexList [0,1,2,0,3]

수열 6번째 수 40 -> DP 배열: [10, 20, 30, 40] (3번 인덱스에 40 삽입), indexList [0,1,2,0,3,3]

 

그 후 indexList의 맨 뒤에서부터 0이 될때까지 가장 먼저 나오는 숫자들만 탐색해보면 부분수열을 찾을 수 있다.

예) indexList [0,1,2,0,3,3]

제일 먼저 나오는 3 -> 5번 인덱스

제일 먼저 나오는 2 -> 2번 인덱스

제일 먼저 나오는 1 -> 1번 인덱스

제일 먼저 나오는 0 -> 0번 인덱스

 

따라서 최장 증가하는 부분 수열은 0,1,2,5번 인덱스인 10,20,30,40 이다.

 

2. O(nlogn) 알고리즘

https://www.acmicpc.net/problem/14003

 

14003번: 가장 긴 증가하는 부분 수열 5

첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다. 둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (-1,000,000,000 ≤ Ai ≤ 1,000,000,000)

www.acmicpc.net

 

n = int(input())
lst = list(map(int,input().split()))
dp = [0]*n
indexList = [-1]*n

maxlen = 0
dp[0] = lst[0]
indexList[0] = 0

for i in range(1,n):
    start = 0
    last = maxlen + 1
    c = 0
    
    while start < last:
        c = int((start+last)/2)
        if dp[c] < lst[i]:
            start = c + 1
        else:
            last = c
    
    dp[start] = lst[i]
    indexList[i] = start
    if start > maxlen:
        maxlen += 1

print(maxlen+1)

stack = []
i = n-1
while maxlen >= 0 and i >= 0:
    if indexList[i] == maxlen:
        stack.append(lst[i])
        maxlen -= 1
    i -= 1
while stack:
    print(stack.pop(),end = " ")

앞선 알고리즘에서 lower_bound를 이용해 시간복잡도를 줄였다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

+ Recent posts