Today I Learned
날짜
2024년 10월 10일 목요일
내용
데이터베이스 설정의 의미
데이터베이스에서 고민이 생겼다.
오른쪽에 있는 네이버 관련된 부분을 작성해야 했는데, 다음과 같은 조건을 만족해야했다.
- 네이버 스토어는 여러개의 네이버 이미지 배너를 가질 수 있다.
- 네이버 이미지 배너는 하나의 네이버 상품과 관련될 수 있다.
- 네이버 이미지 배너는 다수의 인스타그램 게시글과 관련될 수 있다.
- 네이버 상품과 네이버 이미지 배너는 네이버 스토어의 자식테이블이다.
- 중복된 데이터 생성은 최대한 방지한다.
- 이미지 배너 데이터가 삭제되더라도 네이버 상품과 인스타그램 게시글 데이터는 삭제되선 안된다.
어렵다.. 사실 5번만 무시(?) 한다면 일은 훨씬 쉬워진다. 그냥 네이버 배너이미지 테이블 밑에다가 중복된 데이터를 저장하는 테이블을 2개 만들고 관계가 생성되면 복붙해서 넣어주면 된다. 하지만 중복된 데이터는 효율성이나 리소스 낭비의 문제도 있지만 데이터 무결성과도 연관이 있다. 똑같은 데이터가 2개 있을 때, 하나가 삭제되면 반드시 다른 하나가 삭제되야 하는데 그렇지 않게 될 경우 발생할 수 있는 위험성들이라던가..
결국 인스타그램 게시글은 중간 Secondary table을 만들어서 관계를 저장하는 테이블을 만들었다. 꽤 괜찮은 생각이라 느꼈던게, 실제로 저 둘은 다대다 관계다 보니 이게 최선이었다. 네이버 프로덕트도 그냥 관계를 맺어주되, 서로의 삭제에 관여하지 않도록만 설정해주었다. 삭제는 관계만 끊고, 원래 데이터는 건드리지 않도록!
이 코드를 작성하면서 지금까지 데이터베이스를 작성하면서 여러 relationship을 만들어왔었는데, 어떤 특성이 어떻게 쓰이는 건지 전혀 모르고 있었다는 걸 알았다.. 그래서 좀 찾아봤다.
uselist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Parent(Base):
__tablename__ = 'parent'
id = Column(Integer, primary_key=True)
# Child와의 관계 정의
children = relationship('Child', back_populates='parent')
class Child(Base):
__tablename__ = 'child'
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey('parent.id'))
# Parent와의 관계 정의
parent = relationship('Parent', back_populates='children')
위에는 부모 테이블과 자식 테이블이다. 양쪽 모두에 관계를 명시했는데, id를 primary_key로 가진 parent가 부모테이블이 되고, 그 키를 foreign_key(외래키)로 가진 chile 테이블이 자식 테이블이 된다. 여기서 uselist가 있던 없던 기능상으로는 아무 문제가 없다.. 어쩐지 내가 언제는 쓰고 언제는 안썼는데 아무 이상이 없더라니… uselist는 ORM에서만 작동하는 옵션이고 실제 데이터베이스상으로 테이블간의 관계를 설정하지는 못해서 의미가 없다고 한다. 단, 코드의 가독성을 높인다곤 하는데 흠..
uselist를 써서 가독성을 높이면 아래처럼 쓰면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Parent(Base):
__tablename__ = 'parent'
id = Column(Integer, primary_key=True)
# Child와의 관계 정의, 자식은 "여러 명" 이니 uselist=True
children = relationship('Child', back_populates='parent', uselist=True)
class Child(Base):
__tablename__ = 'child'
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey('parent.id'))
# Parent와의 관계 정의, 가급적 부모는 많지 않은게 좋으니 uselist=False
parent = relationship('Parent', back_populates='children', uselist=False)
passive_delete 와 ondelete=”CASCADE”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Parent(Base):
__tablename__ = 'parent'
id = Column(Integer, primary_key=True)
# Child와의 관계 정의
children = relationship(
'Child',
back_populates='parent',
passive_deletes=True # 삭제 시 동작 설정
)
class Child(Base):
__tablename__ = 'child'
id = Column(Integer, primary_key=True)
parent_id = Column(
Integer,
ForeignKey('parent.id', ondelete='CASCADE') # 외래 키에 ondelete 옵션 추가
)
# Parent와의 관계 정의
parent = relationship('Parent', back_populates='children')
passive_delete 는 ORM 수준에서 작동한다. 부모 객체를 삭제했을 떄 ORM이 자식객체도 삭제하는 쿼리문을 생성하지 않도록 해준다. 알아서 데이터베이스가 삭제하도록 냅둔다는 의미다. 따라서 데이터베이스에서 알아서 삭제하도록 잘 정의가 되있어야 하는데, 그게 ondelete="CASCADE"
다.
passive_delete는 데이터베이스에게 삭제하라고 명령하지 않고, 냅두는 설정이므로 반드시 ondelete가 있어야 한다. 경우에 따라 다음과 같이 된다
ondelete="CASCADE"
없이passive_deletes=True
만 설정- ORM: “데이터베이스가 삭제하겠지. 자식 삭제하는 쿼리문은 보내지 말자”
- DB: “몰?루”
- 결론 : 대참사남
ondelete="CASCADE"
를 설정하고passive_deletes=True
도 설정- ORM: “데이터베이스가 삭제하겠지. 자식 삭제하는 쿼리문은 보내지 말자”
- DB: “사전에 약속한대로 부모가 삭제됐으니 알아서 자식도 삭제하자”
- 결론 : 깔끔. 삭제도 데이터베이스 수준에서 처리되니 성능도 좋음
ondelete="CASCADE"
를 설정하지 않고passive_deletes
도 설정하지 않음- ORM: “부모 객체가 삭제됐으니 데이터베이스한테 자식도 삭제하라고 말해주자”
- DB: “ㅇㅋ 삭제하겠음”
- 결론 : 문제는 없음. 다만 쿼리가 추가로 보내지니 2보단 성능은 낮음
그렇다면 동등한 테이블에서 관계는 끊지만 데이터는 삭제하지 않으려면 어떻게 해야할까? 관계가 끊어지면 null로 설정하도록 작성해주면 된다.
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
class Employee(Base):
__tablename__ = 'employee'
id = Column(Integer, primary_key=True)
name = Column(String)
project_id = Column(
Integer,
ForeignKey('project.id', ondelete='SET NULL'),
nullable=True
)
# Project와의 관계 설정
project = relationship(
'Project',
back_populates='employees',
passive_deletes=True
)
class Project(Base):
__tablename__ = 'project'
id = Column(Integer, primary_key=True)
title = Column(String)
# Employee와의 관계 설정
employees = relationship(
'Employee',
back_populates='project',
passive_deletes=True
)
이렇게 되었을 때, 프로젝트가 삭제되더라도 관계된 직원은 삭제되지 않는다. 반대로 프로젝트가 삭제되도 직원은 아무 영향을 안받는다. passive_delete=True
는 서로 삭제의 영향을 안주기 위해 꼭 작성해야 한다. 저걸 안쓰면 한쪽이 삭제되는 순간 ORM이 “야 관계된것도 삭제해라” 라고 쿼리문을 추가로 보낸다!
GPT 길들이기
고객님의 인스타 계정에서 좋은 댓글을 골라 이미지로 만들어줘야 한다. 내가 할 순 없으니 GPT에게 부탁해야 한다. 이전에 알리익스프레스에서 리뷰를 가져와 분석할 때도 사용했었는데 문제가 있었다. 매번 GPT에게 명령을 해야한다. “내가 입력한 댓글들 중에서 10개를 뽑아야 하는데 부정적인건 뽑지 말고, 그렇다고 무조건 좋다는 내용만 뽑기보다는 좀 정성껏 잘쓰인 실사용 후기같은 것지 대답해봐. 근데 답변은 good, bad 둘중 하나로만 해” 라고 매번 요청할 순 없다. 입력 토큰 값 때문에 비용이 어마무시할 거다.. 그리고 경험상 GPT는 이렇게 대답할거다.
“입력한 댓글들 중, 부정적이지 않으면서 실사용 후기에 가까운 댓글인지 확인하고 맞다면 good, 아니면 bad라고 대답하라고 하셨습니다.” ”bad”
저 쓰잘데기 없는 윗문장도 싹다 돈이다. 그래서 이번엔 파인튜닝(fine-tuning)을 해봤다. AI가 내가 원하는 방식대로 작동하도록, 내가 사용하려는 기능을 더 정확하고 일관성있게 수행하도록 트레이닝 시키는 작업이다.
1
코드 비공개
create_file
을 실행하면 openAI 서버에 훈련 데이터를 업로드할 수 있다. 훈련데이터는 jsonl 형식이어야 한다.
1
2
3
{"messages": [{"role": "user", "content": "와… 이걸 어떻게 골라요… 난 못해 …😢넘 이뻐서 엄마가 다 탐나네요. 엄마꺼도 있으면 참 좋겠다아🐧🖤"}, {"role": "assistant", "content": "good"}]}
{"messages": [{"role": "user","content": "프리오더도 품절이에요😭😭"},{"role": "assistant","content": "bad"}]}
JSONL 이니 한줄이 하나의 데이터로 인식된다. 여러 메시지들로 구성되는데, 유저의 입력과 그에 대한 응답값으로 구성된 프롬프트 데이터다. 최소 10개의 데이터는 제공해야 하고, 50개 이상의 질 좋은 프롬프트를 제공하면 웬만하면 잘 작동한다고 한다.
응답은 다음과 같이 온다.
1
2
3
4
5
6
7
8
9
10
11
12
FileObject(
id='***',
bytes=7910,
created_at=1728546887,
filename='file_training.jsonl',
object='file',
purpose='fine-tune',
status='processed',
status_details=None
)
내가 업로드한 파일 id를 입력해서 파인튜닝 작업을 시작하라고 하면 된다(fine_tuning()
).
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
FineTuningJob(
id='***',
created_at=1728547056,
error=Error(code=None,
message=None,
param=None
),
fine_tuned_model=None,
finished_at=None,
hyperparameters=Hyperparameters(
n_epochs='auto',
batch_size='auto',
learning_rate_multiplier='auto'
),
model='gpt-4o-mini-2024-07-18',
object='fine_tuning.job',
organization_id='***',
result_files=[],
seed=1093597155,
status='validating_files', # 훈련 데이터 양식이 제대로 된건지 확인하는 중.
trained_tokens=None,
training_file='***',
validation_file=None,
estimated_finish=None,
integrations=[],
user_provided_suffix=None
)
잘 작동하면 API로 쏴서 결과를 확인하고 모델명을 가져올 수도 있지만, openAI대시보드에서 확인할 수도 있다.
이제 입력해서 판단을 들어보고, 토큰 수는 얼마나 되는지 확인해보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def test_openai(comment: str):
"""
오픈AI 테스트 API
"""
robot = ChatGPTProcessor()
res = robot.make_judgement(comment=comment)
length = robot.calculate_tokens(comment=comment)
return {"판단": res, "토큰 길이": length}
test_oepnai(comment="너무 맘에드는 옷입니다! 근데 사장님 순대국 좋아하세요?")
# 결과
{
"판단": "bad",
"토큰 길이": 21
}
합-격
회고
알찬 하루!