회원가입

이창우 (Beany)

프로젝트 좋아, 코딩 좋아, 만드는 것 자체를 좋아하는 개발자 이창우 입니다~!

### **안녕하세요~!** 👨‍💻
- 🔭 저는 현재 **Django/DRF/Python/백엔드** 개발을 하고 있어요. - 👯 백엔드, 프론트엔드 프로젝트에 함께 협업하고 싶어요. - 🤔 Java 관련해서 도움을 구하고 있습니다. - 📫 연락처: - **Kakao:** cwadvan - **Email:** cwadven@naver.com - ![LinkedIn](https://img.shields.io/badge/linkedin-%230077B5.svg?style=for-the-badge&logo=linkedin&logoColor=white) - Link - ![GitHub](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white) - Link --- **Languages and Tools** DjangoREST ![cwadven's github stats](https://github-readme-stats.vercel.app/api?username=cwadven&show_icons=true&count_private=true&theme=buefy) ![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=cwadven&layout=compact&hide_border=true) --- **MBTI**: `ISTJ`
오늘의 Lesson
Elastic Search

4. Bulk API를 사용하여 여러 문서를 한 번에 색인화하기 ```json POST /my_index/_bulk {"index": {"_id": "1"}} {"title": "Document 1"} {"index": {"_id": "2"}} {"title": "Document 2"} ``` 5. 인덱스 열기 및 닫기를 통해 검색 기능 제어하기 열기: ```json POST /my_index/_open ``` 닫기: ```json POST /my_index/_close ``` 6. Query String Query를 사용하여 유연한 검색 옵션 제공하기 ```json { "query": { "query_string": { "query": "Elasticsearch AND 검색", "default_field": "content" } } } ```

자세히보기
✏ 최근 포스트
💌 좋아요 많은 게시글
python 구글 드라이브 파일 업로드 기능 개발

Google Drive에 파일을 업로드하는 과정을 총 6단계로 나누어 설명하려고 합니다.

  1. 로직 구현을 위한 파일 및 폴더 생성
  2. Google Cloud 프로젝트 생성 및 API 활성화
  3. 사용자 인증 정보 설정
  4. Google Drive에 데이터 저장용 폴더 생성
  5. 로직 구현을 위한 의존성 라이브러리 설치 및 코드 구현
  6. 코드 실행

 

 

1. 로직 구현을 위한 파일 및 폴더 생성


common/common_utils 패키지 내부에 google_utils를 생성하고, google_drive_utils.py 파일을 추가했습니다.

 

 

 

2. Google Cloud 프로젝트 생성 및 API 활성화


https://cloud.google.com/?hl=ko
(Google Drive API Docshttps://developers.google.com/drive/api/reference/rest/v3?apix=true&hl=ko)

API 및 서비스 메뉴에서 라이브러리로 접근합니다.

 

Google Drive API를 검색합니다.

 

Google Drive API 를 선택합니다.

 

사용 클릭~!

 

잠시 기다리면 설정이 완료됩니다~

 

이제 이 API를 사용하려면 사용자 인증 정보가 필요하므로, 사용자 인증 정보를 생성합시다.

 

 

 

3. 사용자 인증 정보 설정


API를 Google Drive API로 설정하고, 애플리케이션 데이터로 설정합니다. 이렇게 설정하는 이유는 파일을 저장하기 위한 목적이기 때문에 사용자의 데이터 접근이 필요하지 않기 때문입니다.

 

서비스 계정의 세부 정보를 설정합니다.

완료 클릭

 

생성한 서비스 계정에서 탭으로 이동하여 새로운를 생성합니다.

이 키는 서버에 저장할 예정이며, 이를 통해 서비스 계정 권한을 부여할 것입니다.

 

JSON 형태로 비공개 키를 생성합니다.

 

만들어진 파일을 잘 저장합니다.

 

 

4. Google Drive에 데이터 저장용 폴더 생성


폴더를 생성합니다.

만든 폴더를 공유합니다.

 

위에 권한을 주기 위해 만들어진 서비스 계정 세부 정보의 주소(3. 사용자 인증 정보 설정 참고)를 넣습니다.

공유합니다.

 

이제 해당 폴더에 들어가서 주소 folders 뒤에 있는 정보를 복사합니다.

 

 

 

 

 

5. 로직 구현을 위한 의존성 라이브러리 설치 및 코드 구현


pip install google-api-python-client google-auth google-auth-oauthlib google-auth-httplib2

 

google_service_account_file.json 이라는 프로젝트 root 쪽에 만들었습니다.

이 폴더 안에는 사용자 정보 json 내용을 붙여넣습니다.(3. 사용자 인증 정보 설정 부분에 다운로드 받은 파일)

 

그 후, settings.py 파일에 GOOGLE_SERVICE_ACCOUNT_FILE 와 GOOGLE_API_SCOPES 를 정의합니다.

(settings.py 에 정의하는 건 상수를 정의하는 거라고 생각하시면 됩니다)

GOOGLE_SERVICE_ACCOUNT_FILE = os.path.join(BASE_DIR, 'google_service_account_file.json')
GOOGLE_API_SCOPES = [
    'https://www.googleapis.com/auth/drive',
]

 

기존에 만들었던 google_drive_utils.py 안에 로직을 추가합니다.

from typing import List

from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build
from googleapiclient.http import (
    MediaFileUpload,
    MediaIoBaseUpload,
)


class GoogleDriveServiceGenerator:
    def __init__(self, account_file_path: str, scopes: List[str]):
        self.account_file_path = account_file_path
        self.scopes = scopes

    def generate_service(self):
        creds = Credentials.from_service_account_file(
            filename=self.account_file_path,
            scopes=self.scopes,
        )
        service = build('drive', 'v3', credentials=creds)
        return service


class GoogleDriveService:
    def __init__(self, service):
        self.service = service

    def get_file_list(self, query: str, page_size: int = 1000) -> List[dict]:
        results = self.service.files().list(
            q=query,
            pageSize=page_size,
            fields="nextPageToken, files(id, name, mimeType)"
        ).execute()
        items = results.get('files', [])
        return items

    def upload_file_by_file_path(self, file_name: str, upload_target_file_path: str, upload_drive_folder_target: str) -> str:
        file_metadata = {
            'name': file_name,
            'parents': [upload_drive_folder_target]
        }
        media = MediaFileUpload(upload_target_file_path, mimetype='application/octet-stream')
        file = self.service.files().create(body=file_metadata, media_body=media, fields='id').execute()
        return file.get('id')

    def upload_file_by_file_obj(self, file_obj, upload_drive_folder_target: str) -> str:
        file_metadata = {
            'name': file_obj.name,
            'parents': [upload_drive_folder_target]
        }
        media = MediaIoBaseUpload(file_obj, mimetype='application/octet-stream')
        file = self.service.files().create(body=file_metadata, media_body=media, fields='id').execute()
        return file.get('id')

    def delete_file(self, file_id: str):
        self.service.files().delete(fileId=file_id).execute()

 

 

 

6. 코드 실행


from common.common_utils.google_utils.google_drive_utils import GoogleDriveService, GoogleDriveServiceGenerator
from django.conf import settings
import os

file_path = os.path.join(settings.BASE_DIR, 'google_service_account_file.json')

google_drive_service = GoogleDriveService(
    service=GoogleDriveServiceGenerator(
        settings.GOOGLE_SERVICE_ACCOUNT_FILE,
        settings.GOOGLE_API_SCOPES,
    ).generate_service()
)
google_drive_service.upload_file_by_file_path(
    file_name='test_aaaaaaa.json',
    upload_target_file_path=file_path,
    upload_drive_folder_target=settings.GOOGLE_DRIVE_MEDIA_BACKUP_FOLDER_ID,
)
'XXXXXXXEXAMPLEIDXH8V'

 

성공적으로 업로드 됐습니다~!

내용을 확인해보니 정상적으로 잘 올라가는 것도 확인했습니다~!

Beany 2024-10-13
1 0
[리팩토링] 8. (2) 메인페이지 좋아요, 댓글 수 Board View 코드 리팩토링

AS-IS Code

def get_board_set_from_board_group(request, board_group_id):
    ...완료!!!


def home(request):
    ...이번장!!!

def board(request, board_url):
    ...

def post_detail(request, board_url, pk):
    ...

def reply_write(request, board_url, pk):
    ...

def rereply_write(request, board_url, pk):
    ...

def reply_delete(request, board_url, pk):
    ...

def rereply_delete(request, board_url, pk):
    ...

def like(request, board_url, pk):
    ...

get_board_set_from_board_group 리팩토링 정보 보기

Board 앱의 View 함수들을 살펴보니 총 9개의 코드가 존재합니다.
하나하나씩 불필요한 코드를 제거하거나 리팩토링해 보겠습니다

 


 

지금은 매번 이 페이지를 조회할 때마다 아래와 같은 코드를 이용해서 좋아요와 댓글 수를 가져오고 있습니다.

def home(request):
    ...
    liked_ordered_post_qs = get_active_posts().select_related(
        'board',
        'author',
    ).annotate(
        reply_count=Count('replys', distinct=True) + Count('rereply', distinct=True),
        like_count=Count('likes', distinct=True),
    ).order_by(
        '-like_count',
        '-reply_count',
        '-id',
    )[:6]

물론 이런 방법으로 데이터를 가져오는 것도 있습니다. 하지만 댓글의 숫자나 좋아요의 숫자가 많아지면 성능이 나빠지는 경향이 있습니다.

지금은 게시글의 좋아요 수와 댓글 수를 각각의 테이블에서 특정 게시글 ID로 조회한 후, 그만큼의 개수를 가져오고 있습니다.

아래 쿼리와 같이 SQL 이 조회가 됩니다.

SELECT "board_post"."id",
       ...
       (COUNT(DISTINCT "board_reply"."id") + COUNT(DISTINCT "board_rereply"."id")) AS "reply_count",
       COUNT(DISTINCT "board_like"."id") AS "like_count",
  FROM "board_post"
  LEFT OUTER JOIN "board_reply"
    ON ("board_post"."id" = "board_reply"."post_id")
  LEFT OUTER JOIN "board_rereply"
    ON ("board_post"."id" = "board_rereply"."post_id")
  LEFT OUTER JOIN "board_like"
    ON ("board_post"."id" = "board_like"."post_id")
 INNER JOIN "board_board"
    ON ("board_post"."board_id" = "board_board"."id")
 INNER JOIN "accounts_user"
    ON ("board_post"."author_id" = "accounts_user"."id")
 WHERE "board_post"."is_active"
 GROUP BY "board_post"."id",
       ...
 ORDER BY "like_count" DESC, "reply_count" DESC, "board_post"."id" DESC
 LIMIT 6

 

다른 방법이 존재합니다.

방식은 이렇습니다.

  1. Post 테이블에 추가적인 컬럼 like_count, reply_count, rereply_count를 만듭니다.
  2. 사용자가 좋아요를 누르거나 좋아요를 취소하는 행동을 하면, Like 테이블에서 Post 기준으로 있는 데이터 개수만큼 조회한 후, like_count를 최신화합니다. (reply_count, rereply_count도 마찬가지입니다.)
  3. 조회할 때, Count SQL 문을 사용하지 않고, Post에 최신화된 like_count, reply_count, rereply_count 컬럼으로 데이터를 보여줍니다.

로직을 조금 복잡하게 할 것인가, 아니면 성능을 더 좋게 할 것인가?

Trade-off로 따지면, 이번 케이스는 후자가 더 좋다고 판단됩니다.

 

이제 코드를 작성해 봅시다.

 

1. 코드 작성


각각의 코드는 아래와 같습니다. 

리펙토링은 나중에 신경 쓰고, 이번에 하려는 작업만 신경 씁시다.

(리펙토링은 다른 게시글에...)

@login_required(login_url='/')
def reply_write(request, board_url, pk):
    if request.method == 'POST':
        post = get_object_or_404(Post, board__url=board_url, pk=pk)
        if request.POST.get('reply_body'):
            Reply.objects.create(post=post, author=request.user, body=request.POST.get('reply_body'))

    return HttpResponseRedirect(reverse('board:post', args=[board_url, pk]))


# 답글 작성
@login_required(login_url='/')
def rereply_write(request, board_url, pk):
    reply = get_object_or_404(Reply, id=pk)
    if request.method == 'POST' and request.POST.get('rereply'):
        rereply = Rereply()
        rereply.reply = reply
        rereply.author = request.user
        rereply.body = request.POST.get('rereply')
        rereply.save()
    return HttpResponseRedirect(reverse('board:post', args=[board_url, reply.post.id]))


# 댓글 삭제
@login_required(login_url='/')
def reply_delete(request, board_url, pk):
    reply = get_object_or_404(Reply, id=pk)
    post_id = reply.post.id
    if reply.author == request.user or request.user.is_superuser:
        reply.delete()
    return HttpResponseRedirect(reverse('board:post', args=[board_url, post_id]))


# 답글 삭제
@login_required(login_url='/')
def rereply_delete(request, board_url, pk):
    rereply = get_object_or_404(Rereply, id=pk)
    post_id = rereply.post.id
    if rereply.author == request.user or request.user.is_superuser:
        rereply.delete()
    return HttpResponseRedirect(reverse('board:post', args=[board_url, post_id]))


# 좋아요 추가 삭제
@login_required(login_url='/')
def like(request, board_url, pk):
    post = get_object_or_404(Post, id=pk)
    qs = Like.objects.filter(author=request.user, post=post)
    if qs.exists():
        qs.delete()
    else:
        Like.objects.create(author=request.user, post=post)
    return HttpResponseRedirect(reverse('board:post', args=[board_url, pk]))

 

먼저 해야하는 작업은 테이블 컬럼 추가합니다.

class Post(TimeStampedModel):
    title = models.CharField(max_length=150)
    body = RichTextUploadingField()
    def_tag = models.CharField(max_length=150, null=True, blank=True)
    post_img = models.ImageField(upload_to='post_img/', null=True, blank=True)
    board = models.ForeignKey(Board, on_delete=models.CASCADE)
    author = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='posts', on_delete=models.CASCADE)
    tag_set = models.ManyToManyField('Tag', blank=True)
    like_count = models.BigIntegerField(default=0, db_index=True)  # 추가
    reply_count = models.BigIntegerField(default=0, db_index=True)  # 추가
    rereply_count = models.BigIntegerField(default=0, db_index=True)  # 추가
    is_active = models.BooleanField(default=True)

 

Service Layer 에 좋아요, 댓글, 대댓글 개수를 업데이트하는 함수를 만듭니다.

def update_post_reply_count(post_id: int) -> None:
    try:
        post = Post.objects.get(id=post_id)
    except Post.DoesNotExist:
        return
    post.reply_count = Reply.objects.filter(post_id=post_id).count()
    post.save(update_fields=('reply_count',))


def update_post_rereply_count(post_id: int) -> None:
    try:
        post = Post.objects.get(id=post_id)
    except Post.DoesNotExist:
        return
    post.rereply_count = Rereply.objects.filter(post_id=post_id).count()
    post.save(update_fields=('rereply_count',))


def update_post_like_count(post_id: int) -> None:
    try:
        post = Post.objects.get(id=post_id)
    except Post.DoesNotExist:
        return
    post.like_count = Like.objects.filter(post_id=post_id).count()
    post.save(update_fields=('like_count',))

 

새로 만든 함수로 View 코드를 수정합니다.

@login_required(login_url='/')
def reply_write(request, board_url, pk):
    if request.method == 'POST':
        post = get_object_or_404(Post, board__url=board_url, pk=pk)
        if request.POST.get('reply_body'):
            Reply.objects.create(post=post, author=request.user, body=request.POST.get('reply_body'))
            update_post_reply_count(pk)  # 여기

    return HttpResponseRedirect(reverse('board:post', args=[board_url, pk]))


# 답글 작성
@login_required(login_url='/')
def rereply_write(request, board_url, pk):
    reply = get_object_or_404(Reply, id=pk)
    if request.method == 'POST' and request.POST.get('rereply'):
        rereply = Rereply()
        rereply.reply = reply
        rereply.author = request.user
        rereply.body = request.POST.get('rereply')
        rereply.save()
        update_post_rereply_count(pk)  # 여기
    return HttpResponseRedirect(reverse('board:post', args=[board_url, reply.post.id]))


# 댓글 삭제
@login_required(login_url='/')
def reply_delete(request, board_url, pk):
    reply = get_object_or_404(Reply, id=pk)
    post_id = reply.post.id
    if reply.author == request.user or request.user.is_superuser:
        reply.delete()
        update_post_reply_count(post_id)  # 여기
    return HttpResponseRedirect(reverse('board:post', args=[board_url, post_id]))


# 답글 삭제
@login_required(login_url='/')
def rereply_delete(request, board_url, pk):
    rereply = get_object_or_404(Rereply, id=pk)
    post_id = rereply.post.id
    if rereply.author == request.user or request.user.is_superuser:
        rereply.delete()
        update_post_rereply_count(post_id)  # 여기
    return HttpResponseRedirect(reverse('board:post', args=[board_url, post_id]))


# 좋아요 추가 삭제
@login_required(login_url='/')
def like(request, board_url, pk):
    post = get_object_or_404(Post, id=pk)
    qs = Like.objects.filter(author=request.user, post=post)
    if qs.exists():
        qs.delete()
    else:
        Like.objects.create(author=request.user, post=post)
    update_post_like_count(pk)  # 여기
    return HttpResponseRedirect(reverse('board:post', args=[board_url, pk]))

 

 

이제 업데이트하는 로직을 추가했으니, 좋아요와 댓글 수를 조회하는 코드를 업데이트합니다.

def home(request):
    ...
    liked_ordered_post_qs = get_active_posts().select_related(
        'board',
        'author',
    ).order_by(
        '-like_count',
        '-reply_count',
        '-id',
    ).only(  # only 이용!
        'id',
        'board__url',
        'author__nickname',
        'title',
        'body',
        'created_at',
        'like_count',
        'reply_count',
        'rereply_count',
    )[:6]
    ...
    liked_ordered_posts=[
        HomePost(
            id=liked_ordered_post.id,
            board_url=liked_ordered_post.board.url,
            title=liked_ordered_post.title,
            body=liked_ordered_post.body,
            like_count=liked_ordered_post.like_count,
            reply_count=liked_ordered_post.reply_count + liked_ordered_post.rereply_count,  # 여기
            author_nickname=liked_ordered_post.author.nickname,
            created_at=liked_ordered_post.created_at.strftime('%Y-%m-%d'),
        )
        for liked_ordered_post in liked_ordered_post_qs
    ],
    ...

 

이제 쿼리를 보면 너무 깔끔합니다.

 

이렇게 한 Controller 안에서 처리하는 방법 말고, Event 방식으로도 업데이트가 가능합니다.
이벤트는 너무 오버스펙이라고 판단하여 사용자가 함수를 호출할 때만 작업하도록 했습니다.

 

끝~!

Beany 2024-05-11
1 0
JIRA 이슈 트래킹 및 워크플로우 통합 방법

이 포스팅의 목적은 JIRA 를 이용해 보다 쉽게 프로젝트 이슈를 어떻게 관리하는지 기록하는 것입니다.

GitHub에서 이루어지는 코드 작업(커밋, 풀 리퀘스트, 브랜치 등)을 JIRA 이슈와 연동하여, 개발 상태를 JIRA 워크플로우 및 프로젝트 관리 프로세스에 자동으로 반영하는 방식입니다.

 

제 블로그 프로젝트 코드를 활용해 이슈 트래킹 연동 작업을 시도해보겠습니다.

Github 링크https://github.com/cwadven/cwbeany 

 

우선, JIRA에 접속하여 회원가입을 진행합니다.

https://www.atlassian.com/ko/software/jira

 

JIRA 상단 메뉴에서 "앱"을 선택한 후 "더 많은 앱 살펴보기"로 들어갑니다.

 

검색창에 "github for jira"를 입력한 뒤 검색하고, "Get it now" 버튼을 클릭합니다.

 

설치가 완료되면 "Get start"를 누르고, 새로 열린 창에서 "Continue"를 클릭합니다.

 

이후 단계를 계속 진행하면 GitHub 계정과 연동할 수 있으며, 계정 연결을 완료합니다.

연동이 성공적으로 이루어지면, 이제 GitHub 프로젝트 설정을 진행합니다.

설정이 마무리되면 JIRA와 GitHub 간의 연동 상태를 확인할 수 있습니다.

 

이제 JIRA에 Task(작업)를 하나 만들어봅시다.

현재는 어떠한 연동도 이루어지지 않은 상태입니다.

 

이제 연동한 레포지토리에서 티켓 번호를 포함해 커밋을 하면, JIRA 이슈와의 연동이 잘 되고 있음을 확인할 수 있습니다.

cwbeany-1-XXXXX (브랜치명)

 

다음 단계로, PR(Pull Request)을 생성하면 해당 티켓의 상태를 자동으로 변경하도록 설정해보겠습니다.

 

우선 JIRA 워크플로우(Workflow)를 단순하게 구성해보겠습니다. 다음과 같이 상태를 6가지로 나누겠습니다.

  • TODO: 아직 시작하지 않은 작업
  • IN DEVELOPMENT: 개발 중이며 PR은 아직 없는 상태
  • CODE REVIEW: PR이 생성된 상태
  • APPROVE: 코드 리뷰가 완료되고 특정 브랜치에 머지(Merge)된 상태
  • LIVE: 배포가 완료된 상태
  • CLOSED: 모든 작업이 완료되어 종료된 상태

 

제가 구현하려는 것은 사용자가 PR을 올리면 티켓 상태가 자동으로 CODE REVIEW로 변경되도록 하는 것입니다. 이 작업은 자동화 기능을 통해 구현할 수 있습니다.

 

PR이 생성되면 티켓 상태를 CODE REVIEW로 전환하도록 설정을 완료했습니다.

 

테스트를 위해 현재 TODO 상태인 티켓에 PR을 생성해보겠습니다.

티켓 상태가 CODE REVIEW로 잘 전환되었습니다!

 

이러한 방식으로 다양한 자동화를 구현할 수 있습니다.
예를 들어, 담당자를 자동으로 지정하거나, 티켓의 개발 시작 날짜를 자동화하는 등의 추가 작업도 가능합니다!

Beany 2024-12-08
0 0
equals() - 동일성과 동등성

equals() - 동일성과 동등성


Object 는 동등성 비교를 위한 equals() 메서드를 제공한다.

 

자바는 두 객체가 같다라는 표현을 2가지로 분리해서 제공한다.

  • 동일성(Identity): == 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인.
  • 동등성(Equality): equals() 메서드를 사용하여 두 객체가 논리적으로 동등한지 확인.

 

단어 정리

"동일"은 완전히 같음을 의미한다. 반면 "동등"은 같은 가치나 수준을 의미하지만 그 형태나 외관 등이 완전히 같지는 않을 수 있다.

쉽게 이야기해서 동일성은 물리적으로 같은 메모리에 있는 객체 인스턴스인지 참조값을 확인하는 것이고, 동등성은 논리적으로 같은지 확인하는 것이다.

동일성은 자바 머신 기준이고 메모리의 참조가 기준이므로 물리적이다. 반면 동등성은 보통 사람이 생각하는 논리적인 기준에 맞추어 비교한다.

예를 들어 같은 회원 번호를 가진 회원 객체가 2개 있다고 가정해보자.

User a = new User("id-100"); // 참조 x001
User b = new User("id-100"); // 참조 x002

이 경우 물리적으로 다른 메모리에 있는 다른 객체이지만, 회원 번호를 기준으로 생각해보면 논리적으로는 같은 회원으로 볼 수 있다.
(주민등록번호가 같다고 가정해도 된다.)

따라서 동일성은 다르지만, 동등성은 같다.

 

문자의 경우도 마찬가지이다.

String s1 = "hello";
String s2 = "hello";

이 경우 물리적으로는 각각의 "hello" 문자열이 다른 메모리에 존재할 수 있지만, 논리적으로는 같은 "hello"라는 문자열이다.
(사실 이 경우 자바가 같은 메모리를 사용할 수 있도록 최적화 한다. 이 부분은 뒤에서 다룬다.)

 

예제를 통해서 동일성동등성을 비교해보자.

package lang.object.equals;

public class UserV1 {
    private String id;

    public UserV1(String id) {
        this.id = id;
    }
}
package lang.object.equals;

public class EqualsMainV1 {
    public static void main(String[] args) {
        UserV1 user1 = new UserV1("user");
        UserV1 user2 = new UserV1("user");

        // user1과 user2는 다른 객체이다.
        System.out.println(user1 == user2);

        // user1과 user2는 다른 객체이다.
        System.out.println(user1.equals(user2));
    }
}
false
false

둘 다 같은 결과가 나옵니다.

초기에 equals 는 아래와 같습니다.

public boolean equals(Object obj) {
    return (this == obj);
}

 

 

equals() - 구현


UserV2 예제

UserV2id(고객번호)가 같으면 논리적으로 같은 객체로 정의하겠다.

package lang.object.equals;


public class UserV2 {
    private String id;

    public UserV2(String id) {
        this.id = id;
    }

    // equals() 메서드를 오버라이딩한다.
    @Override
    public boolean equals(Object obj) {
        UserV2 user = (UserV2) obj;
        return this.id.equals(user.id);
    }
}
package lang.object.equals;

public class EqualsMainV2 {
    public static void main(String[] args) {
        UserV2 user1 = new UserV2("user");
        UserV2 user2 = new UserV2("user");

        // user1과 user2는 다른 객체이다.
        System.out.println(user1 == user2);

        // user1과 user2는 같은 객체이다.
        System.out.println(user1.equals(user2));
    }
}
false
true

 

 

정확한 equals() 구현

앞서 UserV2 에서 구현한 equals() 는 이해를 돕기 위해 매우 간단히 만든 버전이고, 실제로 정확하게 동작하려면 다음과 같이 구현해야한다. 정확한 equals() 메서드를 구현하는 것은 생각보다 쉽지 않다.

IntelliJ를 포함한 대부분의 IDE는 정확한 equals() 코드를 자동으로 만들어준다.

Window: Alt + Insert
Mac: options + N

package lang.object.equals;


import java.util.Objects;

public class UserV2 {
    private String id;

    public UserV2(String id) {
        this.id = id;
    }

    // equals() 메서드를 오버라이딩한다.
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        UserV2 userV2 = (UserV2) o;

        return Objects.equals(id, userV2.id);
    }
}

equals() 메서드를 구현할 때 지켜야 하는 규칙

  • 반사성(Reflexivity): 객체는 자기 자신과 동등해야 한다. (x.equals(x) 는 항상 true)
  • 대칭성(Symmetry): 두 객체가 서로에 대해 동일하다고 판단하면, 이는 양방향으로 동일해야 한다. (x.equals(y) 가 true 이면 y.equals(x) 도 true)
  • 추이성(Transitivity): 만약 한 객체가 두 번째 객체와 동일하고, 두 번째 객체가 세번째 객체와 동일하다면, 첫번째 객체는 세번째 객체와도 동일해야 한다.
  • 일관성(Consistency): 두 객체의 상태가 변경되지 않는 한, equals() 메서드는 항상 동일한 값을 반환해야 한다.
  • null에 대한 비교: 모든 객체는 null 과 비교했을 때 false 를 반환해야 한다.

실무에서는 대부분 IDE가 만들어주는 equals() 를 사용하므로, 이 규칙을 외우기 보다는 대략 이렇구나 정도로 한번 읽어보고 넘어가면 충분하다.

 

정리

  • 참고로 동일성비교가 항상 필요한 것은 아니다. 동일성 비교가 필요한 경우에만 equals() 를 재정의하면 된다.
  • equals()hashCode() 는 보통 함께 사용된다. 이 부분은 뒤에 컬렉션 프레임워크에서 자세히 설명한다.
Beany 2024-12-01
0 0
Object 와 OCP

Object 와 OCP


만약 Object 가 없고, 또 Object 가 제공하는 toString() 이 없다면 서로 아무 관계가 없는 객체의 정보를 출력하기 어려울 것이다. 여기서 아무 관계가 없다는 것은 공통의 부모가 없다는 뜻이다. 아마도 다음의 BadObjectPrinter 클래스와 같이 각각의 클래스마다 별도의 메서드를 작성해야 할 것이다.

package lang.object.tostring;

public class BadObjectPrinter {
    public static void print(Car car) {
        String string = "객체 정보 출력: " + car.carInfo();  // carInfo() 메서드 만듦
        System.out.println(string);
    }

    public static void print(Dog dog) {
        String string = "객체 정보 출력: " + dog.dogInfo();  // dogInfo() 메서드 만듦
        System.out.println(string);
    }
}

 

[ 구체적인 것에 의존 ]

BadObjectPrinter 는 구체적인 타입인 Car, Dog 를 사용한다. 따라서 이후에 출력해야 할 구체적인 클래스가 10개로 늘어나면 구체적인 클래스에 맞추어 메서드도 10개로 계속 늘어나게 된다. 이렇게 BadObjectPrinter 클래스가 구체적인 특정 클래스인 Car, Dog 를 사용하는 것을 BadObjectPrinter Car, Dog 에 의존한다 표현한다.

다행히도 자바에는 객체의 정보를 사용할 때, 다형적 참조 문제를 해결해줄 Object 클래스와 메서드 오버라이딩 문제를 해결해줄 Object.toString() 메서드가 있다. (물론 직접 Object 와 비슷한 공통의 부모 클래스를 만들어서 해결할 수도 있다.)

 

[ 추상적인 것에 의존 ]

우리가 앞서 만든 ObjectPrinter 클래스는 Car, Dog 같은 구체적인 클래스를 사용하는 것이 아니라, 추상적인 Object 클래스를 사용한다. 이렇게 ObjectPrinter 클래스가 Object 클래스를 사용하는 것을 Object 에 클래스에 의존한다고 표현한다.

package lang.object.tostring;

public class ObjectPrinter {
    public static void print(Object object) {
        String string = "객체 정보 출력: " + object.toString();
        System.out.println(object);
    }
}

ObjectPrinter 는 구체적인 것에 의존하는 것이 아니라 추상적인 것에 의존한다.

추상적:여기서 말하는 추상적이라는 뜻은 단순히 추상 클래스나 인터페이스만 뜻하는 것은 아니다. Animal Dog, Cat 의 관계를 떠올려보자. Animal 같은 부모 타입으로 올라갈 수록 개념은 더 추상적이게 되고, Dog, Car 과 같이 하위 타입으로 내려갈 수록 개념은 더 구체적이게 된다.

 

 

자바 언어는 객체지향 언어 답게 언어 스스로도 객체지향의 특징을 매우 잘 활용한다.

우리가 지금까지 배운 toString() 메서드와 같이, 자바 언어가 기본으로 제공하는 다양한 메서드들은 개발자가 필요에 따라 오버라이딩해서 사용할 수 있도록 설계되어 있다.

 

참고 - 정적 의존관계 vs 동적 의존관계

  • 정적 의존관계는 컴파일 시간에 결정되며, 주로 클래스 간의 관계를 의미한다. 앞서 보여준 클래스 의존 관계 그림이 바로 정적 의존관계이다. 쉽게 이야기해서 프로그램을 실행하지 않고, 클래스 내에서 사용하는 타입들만 보면(코드로만 보면!) 쉽게 의존관계를 파악할 수 있다.
  • 동적 의존관계는 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계이다. 앞서 ObjectPrinter.print(Object obj) 에 인자로 어떤 객체가 전달 될지는 프로그램을 실행해봐야 알 수 있다. 어떤 경우에는 Car 인스턴스가 넘어오고, 어떤 경우에는 Dog 인스턴스가 넘어온다. 이렇게 런타임에 어떤 인스턴스를 사용하는지를 나타내는 것이 동적 의존관계이다.
Beany 2024-12-01
0 0
toString()

Object.toString() 메서드는 객체의 정보를 문자열 형태로 제공한다. 그래서 디버깅과 로깅에 유용하게 사용된다.

이 메서드는 Object 클래스에 정의되므로 모든 클래스에서 상속받아 사용할 수 있다.

 

코드로 확인해보자.

package lang.object.tostring;

public class ToStringMain1 {
    public static void main(String[] args) {
        Object object = new Object();
        String string = object.toString();
        
        // toString() 메서드는 객체의 문자열 표현을 반환한다.
        System.out.println(string);
        
        // object 직접 출력
        System.out.println(object);
    }
}
java.lang.Object@10f87f48
java.lang.Object@10f87f48

 

Object.toString()

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
  • Object 가 제공하는 toString() 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)를 16진수로 제공한다.

 

println() 과 toString()

그런데 toString() 의 결과를 출력한 코드와 object println()에 직접 출력한 코드의 결과가 완전히 같다.

Object object = new Object();
String string = object.toString();

// toString() 메서드는 객체의 문자열 표현을 반환한다.
System.out.println(string);

// object 직접 출력
System.out.println(object);

System.out.println() 메서드는 사실 내부에서 toString() 을 호출한다.

Object 타입(자식 포함)이 println() 에 인수로 전달되면 내부에서 obj.toString() 메서드를 호출해서 결과를 출력한다.

public void println(Object x) {
     String a String.valueOf(x);
}
public static String valueOf (Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

따라서 println() 을 사용할 때, toString() 을 직접 호출할 필요 없이 객체를 바로 전달하면 객체의 정보를 출력할 수 있다.

 

 

toString() 오버라이딩

Object.toString() 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나타내지 못한다. 그래서 보통 toString() 재정의(오버라이딩)해서 보다 유용한 정보를 제공하는 것이 일반적이다.

package lang.object.tostring;

public class Car {
    private String carName;

    public Car(String carName) {
        this.carName = carName;
    }
}
package lang.object.tostring;

public class Dog {
    private String dogName;
    private int age;

    public Dog(String dogName, int age) {
        this.dogName = dogName;
        this.age = age;
    }

    // toString() 메서드를 오버라이딩한다.
    @Override
    public String toString() {
        return "Dog{" +
                "dogName='" + dogName + '\'' +
                ", age=" + age +
                '}';
    }
}
package lang.object.tostring;

public class ToStringMain2 {
    public static void main(String[] args) {
        Dog dog = new Dog("멍멍이", 3);
        System.out.println(dog);

        Car car = new Car("람보르기니");
        System.out.println(car);
    }
}
Dog{dogName='멍멍이', age=3}
lang.object.tostring.Car@133314b

 

Beany 2024-12-01
0 0
About Beany's Blog
Beany의 개인 블로그 입니다~! 개인 일상/프로젝트/자료 등 잡다한 내용이 담길 블로그 입니다! 잘 부탁드립니다~!
Yesterday: 177
Today: 448