개발 이야기 안하는 개발자

1장 : 리팩터링 본문

Book/리팩터링 2판

1장 : 리팩터링

07e 2023. 5. 27. 20:01
반응형

리팩터링 책 독후감

http://www.yes24.com/Product/Goods/89649360

 

리팩토링 들어가 보자

 

Image by <a href="https://pixabay.com/users/polyankakul-5176825/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=2252316">Полинка</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=2252316">Pixabay</a>

 

리팩터링

다른 사람이 읽고 이해해야 할 일이 생겼는데 읽고 파악하기 어렵다면 시간이 오래걸리고 문제가 생길 수 있기 때문에 이를 방지하고자 읽기 쉽고 파악하기 쉬운 코드로 수정해주는 기법이다.

 

리팩터링을 하면 코드 줄이 늘어날 수 있다. 하지만 각 기능에 맞는 코드를 모듈화하면서 늘어난 코드는 다른 사람이 보기에 더 빨른 이해를 돕기때문에 코드 자체는 늘어났지만 다른 사람이 사용하기엔 더 빠르게 이해하고 작업을 할 수 있게 돕는다.

 

- 테스트

리팩터링하기 전엔 제대로 된 테스트부터 마련해야 한다.

리팩터링은 코드를 이해하기 쉽게 기존에 있던 코드를 수정하는 내용이다.

즉, 새로운 기능을 개발하는 것이 아니기 때문에 수정하기 전의 모든 기능을 수정 후에도 가능해야 한다.

테스트를 계속해서 진행해야 하며, 리팩터링이 된 후에도 기능이 정상작동하는지 계속해서 확인해 봐야 한다.

 

- 함수를 추출한다 

코드를 읽다가 For문이나 Switch 같은 곳에서 해당 코드를 읽어야만 해당 내용을 이해할 수 있는 것들은 함수로 추출해서 이해하기 쉽게 만드는게 가독성을 높인다.

이런식으로 추출한 함수에는 result라는 변수를 써서 해당 함수의 결과값을 따라가게 보기 쉽게 하는게 좋다.

 

- 느낀점

1장에선 리팩토링이 어떤방식으로 진행되는지 예시를 보여 주었는데, 읽고 나서 리팩토링이 뭔지 느낌이 확 와닿았다.

그래서 이 책을 다 읽기전, 1챕터만 읽고 한번 해볼 예정이다.

이 책을 다 읽고나서 지금 이걸 보면 어떤 느낌일지 아주 궁금하다

 

 

---

여기서 부터는 내가 대학시절 짰던 코드를 리팩토링 해 볼 예정이다.

https://github.com/727207e/GraphicCapstone_Non_Euclidean/blob/main/Assets/Scripts/Portal/PortalTeleporter.cs

 

GitHub - 727207e/GraphicCapstone_Non_Euclidean

Contribute to 727207e/GraphicCapstone_Non_Euclidean development by creating an account on GitHub.

github.com

 

아래는 해당 코드이다.

Portal 게임을 만들려고 순간이동하는 내용에 대해 작성한 코드인듯 하다. (오래되서 기억 안남)

진짜 날것 그대로 복사해서 가져왔다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PortalTeleporter : MonoBehaviour
{

    public Transform player;
    public Transform reciever;

    public bool playerIsOverlapping = false;

    // Update is called once per frame
    void Update()
    {
        if (playerIsOverlapping)
        {
            Vector3 portalToPlayer = player.position - transform.position;
            float dotProduct = Vector3.Dot(transform.up, portalToPlayer);

            // If this is true: The player has moved across the portal
            if (dotProduct < 0f)
            {

                //중력 확인
                if (!player.transform.Find("Main Camera").GetComponent<CameraGravityInfo>().CameraIsInGravity)
                {
                    //////거리
                    ///

                    //플레이어와 포탈의 거리를 잰다.
                    Vector3 playerOffsetFromPortal = player.position - transform.position;

                    ////해당 스크립트를 가지고 있는 카메라가 비추는 정면의 포탈 각도를 구한다.
                    Vector3 therot = reciever.transform.eulerAngles - transform.eulerAngles;

                    //포탈이 180도 돌아간 상태로 (in, out)이 설정되어 있음으로 그 값은 빼준다.
                    therot.y += 180;

                    //포탈의 거리 벡터를 회전각도만큼 같이 회전시킨다.
                    playerOffsetFromPortal = Quaternion.Euler(therot.x, therot.y, therot.z) * playerOffsetFromPortal;

                    //플레이어 위치 이동
                    player.transform.position = reciever.transform.position + playerOffsetFromPortal;

                    //////회전
                    ///


                    //바라보는 각도
                    Vector3 PlayerLookPoint = Quaternion.Euler(therot.x, therot.y, therot.z) * player.forward;

                    player.transform.rotation = Quaternion.LookRotation(PlayerLookPoint, Vector3.up);

                }

                else
                {
                    //////거리
                    ///

                    //플레이어와 포탈의 거리를 잰다.
                    Vector3 playerOffsetFromPortal = player.position - transform.position;

                    ////해당 스크립트를 가지고 있는 카메라가 비추는 정면의 포탈 각도를 구한다.
                    Vector3 therot = reciever.transform.eulerAngles - transform.eulerAngles;

                    //포탈이 180도 돌아간 상태로 (in, out)이 설정되어 있음으로 그 값은 빼준다.
                    therot.y += 180;

                    //포탈의 거리 벡터를 회전각도만큼 같이 회전시킨다.
                    //playerOffsetFromPortal = Quaternion.Euler(therot.x, therot.y, therot.z) * playerOffsetFromPortal;

                    //플레이어 위치 이동
                    player.transform.position = reciever.transform.position + playerOffsetFromPortal;



                    //////회전
                    ///


                    player.transform.eulerAngles = new Vector3(0, player.transform.eulerAngles.y, 180f);

                }

                playerIsOverlapping = false;

            }
        }
    }

    void OnTriggerEnter(Collider other)
    {
        if (other.tag == "Player")
        {
            playerIsOverlapping = true;
        }
    }

    void OnTriggerExit(Collider other)
    {
        if (other.tag == "Player")
        {
            playerIsOverlapping = false;
        }
    }
}

https://tenor.com/ko/view/selambackstory-backstory-backstoryyt-g%C3%BClmemeliyim-dont-laugh-gif-26010909

 

내용을 보니 포탈 내부로 부터 이동하면 자연스럽게 위치를 이동시키위한 코드 같다.

해당 내용을 리팩토링 해보자.

 

  • 우선 Update문 안에 있는 내용을 메서드로 빼보자.

    // Update is called once per frame
    void Update()
    {
        PlayerTeleport();
    }

    private void PlayerTeleport()
    {
        (...)
    }

이 내용이 더 좋아보인다.

메 프레임마다 PlayerTeleport()를 호출할 것이라는 것이 더 명확하게 이해할 수 있어서 좋아보인다.

 


PlayerTeleport 내용을 보면 매 프레임단위로 내적을 활용해서 포탈 넘어로 갔는지 확인하는 내용이다.

  • 내적 이후에 내용들을 함수로 만드는게 더 좋아보이니 함수로 뽑아보자.
    private void PlayerTeleport()
    {
        if (playerIsOverlapping)
        {
            Vector3 portalToPlayer = player.position - transform.position;
            float dotProduct = Vector3.Dot(transform.up, portalToPlayer);

            // If this is true: The player has moved across the portal
            if (dotProduct < 0f)
            {
                PlayerPositionChange();
            }
        }
    }

    private void PlayerPositionChange()
    {
        (...)
    }

 


코드를 보니 내적을 하고 나서 연산을 해야하는 것 같아 보인다.

  • 그럼 아예 처음부터 내적을 구하는 메서드를 만들어서 정리해보자.
    private void PlayerTeleport()
    {
        if (playerIsOverlapping)
        {
            float dotProduct = DotPlayerAndPortal();

            // If this is true: The player has moved across the portal
            if (dotProduct < 0f)
            {
                PlayerPositionChange();
            }
        }
    }

    private float DotPlayerAndPortal()
    {
        Vector3 portalToPlayer = player.position - transform.position;
        return Vector3.Dot(transform.up, portalToPlayer);
    }

 

 


  • 어차피 값이 하나밖에 없기 때문에 해당 메서드 호출을 그냥 if문 안에 넣어버리자.
    private void PlayerTeleport()
    {
        if (playerIsOverlapping)
        {
            // true: The player has moved across the portal
            if (DotPlayerAndPortal() < 0f)
            {
                PlayerPositionChange();
            }
        }
    }

이게 더 깔끔해 보인다.

 

Player Teleport 메서드는 

만약 player가 포탈과 겹쳐졌다면 다음을 진행한다.

Player와 Portal의 내적을 구해서 0보다 작다면 PlayerPositionChange를 진행한다

 


이제 PlayerPositionChange() 를 보자. 보면 안에 중복되는 코드들이 있다.

  • 해당 코드들을 If문 위로 꺼내보자.
    private void PlayerPositionChange()
    {
        //////거리
        //플레이어와 포탈의 거리를 잰다.
        Vector3 playerOffsetFromPortal = player.position - transform.position;

        ////해당 스크립트를 가지고 있는 카메라가 비추는 정면의 포탈 각도를 구한다.
        Vector3 therot = reciever.transform.eulerAngles - transform.eulerAngles;

        //포탈이 180도 돌아간 상태로 (in, out)이 설정되어 있음으로 그 값은 빼준다.
        therot.y += 180;

        //중력 확인
        if (!player.transform.Find("Main Camera").GetComponent<CameraGravityInfo>().CameraIsInGravity)
        {
            //포탈의 거리 벡터를 회전각도만큼 같이 회전시킨다.
            playerOffsetFromPortal = Quaternion.Euler(therot.x, therot.y, therot.z) * playerOffsetFromPortal;

            //플레이어 위치 이동
            player.transform.position = reciever.transform.position + playerOffsetFromPortal;
            //////회전
            //바라보는 각도
            Vector3 PlayerLookPoint = Quaternion.Euler(therot.x, therot.y, therot.z) * player.forward;

            player.transform.rotation = Quaternion.LookRotation(PlayerLookPoint, Vector3.up);
        }
        else
        {
            //플레이어 위치 이동
            player.transform.position = reciever.transform.position + playerOffsetFromPortal;
            //////회전
            player.transform.eulerAngles = new Vector3(0, player.transform.eulerAngles.y, 180f);
        }

        playerIsOverlapping = false;
    }

player.transform.position 이라는 플레이어 위치 이동값이 여전히 겹치긴 하지만 

해당 내용은 이동과 회전이 붙어있는게 더 좋아보여서 이렇게 두었다.

 


중력 조건에 따라서 위치값과 회전값이 바뀌는 모양이다.

그럼 위치값 변환과 회전값 변환 메서드를 각각 만들어 넣어주면 어떨까

 

...라고 생각했는데 지금 보니까 회전도가 계산하는 방식이 다르다는것을 깨닳았다.

  • 그럼 중력 조건 true false로 나눠야겠다.
    private void PlayerPositionChange()
    {
        //플레이어와 포탈의 거리를 잰다.
        Vector3 playerOffsetFromPortal = player.position - transform.position;

        //중력 확인
        if (!player.transform.Find("Main Camera").GetComponent<CameraGravityInfo>().CameraIsInGravity)
        {
            PlayerTransformTeleportInGravity(playerOffsetFromPortal);
        }
        else
        {
            PlayerTransformTeleportOutGravity(playerOffsetFromPortal);
        }

        playerIsOverlapping = false;
    }

    private void PlayerTransformTeleportInGravity(Vector3 offset)
    {
        //해당 스크립트를 가지고 있는 카메라가 비추는 정면의 포탈 각도를 구한다.
        Vector3 portalRot = reciever.transform.eulerAngles - transform.eulerAngles;
        //포탈이 180도 돌아간 상태로 (in, out)이 설정되어 있음으로 그 값은 빼준다.
        portalRot.y += 180;

        //포탈의 거리 벡터를 회전각도만큼 같이 회전시킨다.
        offset = Quaternion.Euler(portalRot.x, portalRot.y, portalRot.z) * offset;
        //바라보는 각도
        Vector3 PlayerLookPoint = Quaternion.Euler(portalRot.x, portalRot.y, portalRot.z) * player.forward;

        //플레이어 이동
        player.transform.position = reciever.transform.position + offset;
        player.transform.rotation = Quaternion.LookRotation(PlayerLookPoint, Vector3.up);
    }

    private void PlayerTransformTeleportOutGravity(Vector3 offset)
    {
        //플레이어 이동
        player.transform.position = reciever.transform.position + offset;
        player.transform.eulerAngles = new Vector3(0, player.transform.eulerAngles.y, 180f);
    }

하다보니 therot 이라는 변수는 중력 안에 있을때만 연산하는 내용이라 위치를 수정했다.

변수 이름도 마음에 안들어서 수정했다.

 


저기 PlayerPositionChange() 안에 있는 마지막 playerIsOverraping = false 가 저 메서드 안에 있는게 옳지 못하다고 생각했다. 메서드 역할에 포함되는 변수가 아니기 때문이다.

 

그래서 나온 최종본이다

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PortalTeleporter : MonoBehaviour
{
    public Transform player;
    public Transform reciever;

    public bool playerIsOverlapping = false;

    // Update is called once per frame
    void Update()
    {
        PlayerTeleport();
    }

    private void PlayerTeleport()
    {
        if (playerIsOverlapping)
        {
            // true: The player has moved across the portal
            if (DotPlayerAndPortal() < 0f)
            {
                PlayerPositionChange();
                playerIsOverlapping = false;
            }
        }
    }

    private float DotPlayerAndPortal()
    {
        Vector3 portalToPlayer = player.position - transform.position;
        return Vector3.Dot(transform.up, portalToPlayer);
    }

    private void PlayerPositionChange()
    {
        //플레이어와 포탈의 거리를 잰다.
        Vector3 playerOffsetFromPortal = player.position - transform.position;

        //중력 확인
        if (!player.transform.Find("Main Camera").GetComponent<CameraGravityInfo>().CameraIsInGravity)
        {
            PlayerTransformTeleportInGravity(playerOffsetFromPortal);
        }
        else
        {
            PlayerTransformTeleportOutGravity(playerOffsetFromPortal);
        }
    }

    private void PlayerTransformTeleportInGravity(Vector3 offset)
    {
        //해당 스크립트를 가지고 있는 카메라가 비추는 정면의 포탈 각도를 구한다.
        Vector3 portalRot = reciever.transform.eulerAngles - transform.eulerAngles;
        //포탈이 180도 돌아간 상태로 (in, out)이 설정되어 있음으로 그 값은 빼준다.
        portalRot.y += 180;

        //포탈의 거리 벡터를 회전각도만큼 같이 회전시킨다.
        offset = Quaternion.Euler(portalRot.x, portalRot.y, portalRot.z) * offset;
        //바라보는 각도
        Vector3 PlayerLookPoint = Quaternion.Euler(portalRot.x, portalRot.y, portalRot.z) * player.forward;

        //플레이어 이동
        player.transform.position = reciever.transform.position + offset;
        player.transform.rotation = Quaternion.LookRotation(PlayerLookPoint, Vector3.up);
    }

    private void PlayerTransformTeleportOutGravity(Vector3 offset)
    {
        //플레이어 이동
        player.transform.position = reciever.transform.position + offset;
        player.transform.eulerAngles = new Vector3(0, player.transform.eulerAngles.y, 180f);
    }

    void OnTriggerEnter(Collider other)
    {
        if (other.tag == "Player")
        {
            playerIsOverlapping = true;
        }
    }

    void OnTriggerExit(Collider other)
    {
        if (other.tag == "Player")
        {
            playerIsOverlapping = false;
        }
    }
}

 

최종본은 이러한데 아직 1장밖에 안읽기도 했고 저기 PlayerTransformTeleportIntGravity 메소드가 맞는지 모르겠다.

뭐 책 다읽으면 해당 리팩토링을 체점해볼 생각이다.

 

내가 많이 안해본 거기도 하고 보고 이상하다 싶으면 내가 상처받지 않을 선으로 훈수 바란다.

 

근데 사실 개인적으로 만족스럽긴 하다.

코드가 많이 깨끗해진것같아 보인다.

 

https://tenor.com/ko/view/shaq-shimmy-excited-smile-gif-12032436

 

만 ㅡ 족

반응형

'Book > 리팩터링 2판' 카테고리의 다른 글

12장 상속 다루기  (0) 2023.06.15
4장 테스트 구축하기  (0) 2023.06.09
3장 코드에서 나는 악취  (2) 2023.06.09
2장 리팩터링 원칙  (0) 2023.05.27